beadhive 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 (64) hide show
  1. beadhive/__init__.py +1 -0
  2. beadhive/adopt.py +143 -0
  3. beadhive/archive.py +165 -0
  4. beadhive/assets/AGF-hint.md +10 -0
  5. beadhive/assets/PRIME.md +131 -0
  6. beadhive/assets/claude-settings.json +21 -0
  7. beadhive/assets/observaloop/bh-dashboard.json +1100 -0
  8. beadhive/assets/observaloop/cli-metrics-preset.yaml +49 -0
  9. beadhive/bd.py +154 -0
  10. beadhive/cli.py +1484 -0
  11. beadhive/config.py +1304 -0
  12. beadhive/doctor.py +675 -0
  13. beadhive/dolt.py +130 -0
  14. beadhive/escalate.py +153 -0
  15. beadhive/git.py +45 -0
  16. beadhive/gitworkspace.py +86 -0
  17. beadhive/guard.py +292 -0
  18. beadhive/hq.py +53 -0
  19. beadhive/hub.py +202 -0
  20. beadhive/identity.py +114 -0
  21. beadhive/log.py +176 -0
  22. beadhive/mcp.py +875 -0
  23. beadhive/metadata.py +420 -0
  24. beadhive/molecule.py +333 -0
  25. beadhive/observaloop.py +465 -0
  26. beadhive/observaloop_env.py +170 -0
  27. beadhive/onboard.py +735 -0
  28. beadhive/orca.py +655 -0
  29. beadhive/otel.py +958 -0
  30. beadhive/otel_lgtm.py +75 -0
  31. beadhive/plan.py +966 -0
  32. beadhive/plugins.py +64 -0
  33. beadhive/registry.py +425 -0
  34. beadhive/report.py +191 -0
  35. beadhive/report_target.py +120 -0
  36. beadhive/retire.py +368 -0
  37. beadhive/rig.py +658 -0
  38. beadhive/rig_migrate.py +167 -0
  39. beadhive/rig_ready.py +268 -0
  40. beadhive/role.py +151 -0
  41. beadhive/route.py +90 -0
  42. beadhive/run.py +78 -0
  43. beadhive/safety.py +1325 -0
  44. beadhive/schedule.py +294 -0
  45. beadhive/setup.py +218 -0
  46. beadhive/state.py +154 -0
  47. beadhive/survey.py +270 -0
  48. beadhive/templates/config.example.yaml +183 -0
  49. beadhive/templates/docker-compose.otel.yml +30 -0
  50. beadhive/templates/docker-compose.yml +24 -0
  51. beadhive/templates/env.example +20 -0
  52. beadhive/triage.py +293 -0
  53. beadhive/validate.py +116 -0
  54. beadhive/work.py +1801 -0
  55. beadhive/work_group.py +258 -0
  56. beadhive/work_logic.py +200 -0
  57. beadhive/work_show.py +213 -0
  58. beadhive/worktree.py +1516 -0
  59. beadhive/worktree_merge.py +238 -0
  60. beadhive/wt_status.py +275 -0
  61. beadhive-0.1.0.dist-info/METADATA +84 -0
  62. beadhive-0.1.0.dist-info/RECORD +64 -0
  63. beadhive-0.1.0.dist-info/WHEEL +4 -0
  64. beadhive-0.1.0.dist-info/entry_points.txt +3 -0
beadhive/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """ws — workspace beads CLI (per-repo rigs + workspace hydration)."""
beadhive/adopt.py ADDED
@@ -0,0 +1,143 @@
1
+ """`ws plan adopt` — seed a plan FRAME from promoted intake report(s).
2
+
3
+ The planning-plane ADOPT path (epic, bead). It is the
4
+ planner-side consumer of the triage promote disposition: a report handed
5
+ to the planner carries ``intake:promoted`` (``state.is_promoted``), and ``adopt`` fleshes it into
6
+ the opening FRAME of a molecule spec — the epic seed a planner then decomposes into issues and
7
+ files via ``ws plan file``. It is source-agnostic: any channel (report / github / import) that
8
+ was promoted is adoptable.
9
+
10
+ Two provenance facets carry through to the filed epic (see ``ws/state.py``):
11
+
12
+ * **System-of-record** — the NATIVE ``source_system`` + ``external_ref`` pair (e.g. github /
13
+ ``gh-9``) survives onto the epic so a GitHub-sourced request stays traceable. ``source_system``
14
+ is settable only at bead birth, so an adopted epic that carries it is born via ``bd import``
15
+ (``plan._create_epic``); ``bd create``/``update`` expose no flag for it.
16
+ * **Originating link** — on ``ws plan file`` each origin report is linked as CHILD-OF the epic
17
+ (report depends-on epic, ``parent-child``). The epic OWNS the report, never the reverse — so
18
+ the report is NEVER a blocker of the epic (it can't wrongly gate the molecule on an open
19
+ report) and it rides the epic to completion. A ``blocks`` edge is not usable here: bd forbids
20
+ blocking dependencies between an epic and a task, so ``parent-child`` is the sanctioned link.
21
+
22
+ This module is Typer-free and bd-free — pure spec shaping over already-read bead JSON. ``plan.py``
23
+ owns the CLI verb plus every bd read/write (the ``plan.run`` test seam), so the two planes share
24
+ one subprocess seam and the shaping logic stays trivially unit-testable.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from . import state
30
+
31
+ # Native system-of-record provenance fields carried from an origin report onto the filed epic.
32
+ PROVENANCE_FIELDS = ("source_system", "external_ref")
33
+
34
+
35
+ class AdoptError(Exception):
36
+ """Frame seeding failed (e.g. no beads to adopt). Typer-free; the CLI maps it to exit 1."""
37
+
38
+
39
+ # ---- frame seeding (pure; over bead JSON already read by the caller) ---------
40
+
41
+
42
+ def _seed_title(beads: list[dict]) -> str:
43
+ """The seed epic title: ``Adopt: <first report title>`` (+ a count when several are folded)."""
44
+ first = str(beads[0].get("title") or beads[0].get("id") or "untitled").strip()
45
+ extra = len(beads) - 1
46
+ suffix = f" (+{extra} more)" if extra > 0 else ""
47
+ return f"Adopt: {first}{suffix}"
48
+
49
+
50
+ def _seed_description(beads: list[dict]) -> str:
51
+ """Seed the epic description from the report text(s) so the planner opens with the full ask."""
52
+ lines = ["Adopted from promoted intake report(s):", ""]
53
+ for bead in beads:
54
+ bid = str(bead.get("id") or "?")
55
+ title = str(bead.get("title") or "").strip()
56
+ lines.append(f"- {bid}: {title}".rstrip())
57
+ body = str(bead.get("description") or "").strip()
58
+ if body:
59
+ lines.append(f" {body}")
60
+ return "\n".join(lines)
61
+
62
+
63
+ def _provenance(beads: list[dict]) -> tuple[str, str]:
64
+ """The system-of-record provenance to carry onto the epic: the ``(source_system, external_ref)``
65
+ of the FIRST report that carries either (a GitHub-sourced report keeps its ``gh-<n>`` trace).
66
+ ``('', '')`` when no report carries native provenance (a born-native cross-rig report)."""
67
+ for bead in beads:
68
+ source_system = str(bead.get("source_system") or "").strip()
69
+ external_ref = str(bead.get("external_ref") or "").strip()
70
+ if source_system or external_ref:
71
+ return source_system, external_ref
72
+ return "", ""
73
+
74
+
75
+ def frame_from_beads(beads: list[dict]) -> dict:
76
+ """Build the seed molecule FRAME (epic seed + empty ``issues``) from promoted report bead(s).
77
+
78
+ The epic records the originating report id(s) under ``adopts`` and any native provenance under
79
+ ``source_system`` / ``external_ref``; ``ws plan file`` reads these to link + carry them. Issues
80
+ are left empty for the planner to decompose. Typer-free; raises ``AdoptError`` on empty input.
81
+ """
82
+ if not beads:
83
+ raise AdoptError("no intake beads to adopt")
84
+ epic: dict = {
85
+ "title": _seed_title(beads),
86
+ "description": _seed_description(beads),
87
+ "adopts": [str(b.get("id")) for b in beads if b.get("id")],
88
+ }
89
+ source_system, external_ref = _provenance(beads)
90
+ if source_system:
91
+ epic["source_system"] = source_system
92
+ if external_ref:
93
+ epic["external_ref"] = external_ref
94
+ return {"epic": epic, "issues": []}
95
+
96
+
97
+ # ---- file-time helpers (read by plan.file_molecule; pure) --------------------
98
+
99
+
100
+ def adopts_of(epic: dict) -> list[str]:
101
+ """The originating report id(s) an adopted epic links back to (``[]`` when not adopted)."""
102
+ return [str(x) for x in (epic.get("adopts") or []) if str(x).strip()]
103
+
104
+
105
+ def provenance_of(epic: dict) -> tuple[str, str]:
106
+ """The ``(source_system, external_ref)`` provenance declared on an epic (``''`` when unset)."""
107
+ return (
108
+ str(epic.get("source_system") or "").strip(),
109
+ str(epic.get("external_ref") or "").strip(),
110
+ )
111
+
112
+
113
+ def epic_import_record(epic: dict, labels: list[str]) -> dict:
114
+ """A ``bd import`` JSONL record that BIRTHS the epic carrying native provenance.
115
+
116
+ ``source_system`` can only be set when a bead is created, so a provenance-carrying epic is
117
+ imported rather than ``bd create``-d. Carries title/type + description/design + the identity
118
+ triplet labels + the native ``source_system`` / ``external_ref`` pair.
119
+ """
120
+ record: dict = {"title": str(epic.get("title") or ""), "issue_type": "epic"}
121
+ for key in ("description", "design"):
122
+ val = str(epic.get(key) or "").strip()
123
+ if val:
124
+ record[key] = val
125
+ source_system, external_ref = provenance_of(epic)
126
+ if source_system:
127
+ record["source_system"] = source_system
128
+ if external_ref:
129
+ record["external_ref"] = external_ref
130
+ if labels:
131
+ record["labels"] = list(labels)
132
+ return record
133
+
134
+
135
+ # ---- read-side: distinguish an origin report from a work sibling -------------
136
+
137
+
138
+ def is_origin_report(labels) -> bool:
139
+ """True for an ADOPTED origin report linked under an epic — a promoted intake bead
140
+ (``intake:promoted``) or one carrying an ``origin:`` channel label. Used to keep origin reports
141
+ OUT of the molecule's work-sibling set (they carry no acceptance and demand no kickoff gate)
142
+ while still surfacing them in ``ws plan show``."""
143
+ return state.is_promoted(labels) or state.origin_of(labels) is not None
beadhive/archive.py ADDED
@@ -0,0 +1,165 @@
1
+ """archive.py — list and prune the soft-archive graveyard created by ``ws rig retire``.
2
+
3
+ ``ws rig retire`` moves retired clones into ``archive.dir`` (default
4
+ ``$GIT_WORKSPACE/.archived``) under a ``<provider>/<org>/<repo>`` subpath. This module
5
+ provides the read and reclaim commands:
6
+
7
+ - ``list_archived(archive_dir)`` → ``list[ArchivedRepo]`` sorted by descending age.
8
+ - ``prune_archived(archive_dir, *, older_than_days, all, dry_run)`` → ``PruneResult``.
9
+
10
+ Guard: ``prune_archived`` resolves every candidate path and asserts it is strictly inside
11
+ ``archive_dir`` before calling ``shutil.rmtree`` — so a misconfigured or symlinked
12
+ ``archive.dir`` can never cause collateral damage outside the graveyard.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import shutil
19
+ import time
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Helpers
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ def _dir_size(path: Path) -> int:
29
+ """Recursively sum the sizes of all files under ``path``."""
30
+ total = 0
31
+ try:
32
+ for root, _dirs, files in os.walk(path):
33
+ for f in files:
34
+ try:
35
+ total += (Path(root) / f).stat().st_size
36
+ except OSError:
37
+ pass
38
+ except (OSError, PermissionError):
39
+ pass
40
+ return total
41
+
42
+
43
+ def _age_days(path: Path) -> float:
44
+ """Age of ``path`` in fractional days (mtime-based)."""
45
+ try:
46
+ mtime = path.stat().st_mtime
47
+ except OSError:
48
+ return 0.0
49
+ return (time.time() - mtime) / 86400.0
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Data types
54
+ # ---------------------------------------------------------------------------
55
+
56
+
57
+ @dataclass
58
+ class ArchivedRepo:
59
+ """One archived clone entry under ``archive_dir``."""
60
+
61
+ path: Path
62
+ """Absolute path of the archived clone (``<archive_dir>/<provider>/<org>/<repo>``)."""
63
+
64
+ triplet: str
65
+ """``<provider>/<org>/<repo>`` — the human-readable identity."""
66
+
67
+ age_days: float
68
+ """Fractional days since the directory was last modified (mtime)."""
69
+
70
+ size_bytes: int
71
+ """Total size of all files under the clone directory."""
72
+
73
+
74
+ @dataclass
75
+ class PruneResult:
76
+ """Outcome of ``prune_archived``."""
77
+
78
+ removed: list[str] = field(default_factory=list)
79
+ """Triplets removed (or would-remove under dry-run)."""
80
+
81
+ reclaimed_bytes: int = 0
82
+ """Total bytes freed (0 on dry-run — nothing was actually removed)."""
83
+
84
+ dry_run: bool = False
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Core logic
89
+ # ---------------------------------------------------------------------------
90
+
91
+
92
+ def list_archived(archive_dir: Path) -> list[ArchivedRepo]:
93
+ """Return all ``<provider>/<org>/<repo>`` entries under ``archive_dir``, sorted by age
94
+ (oldest first — matches the prune ordering so the output and pruning are consistent).
95
+
96
+ Returns an empty list when ``archive_dir`` does not exist.
97
+ """
98
+ if not archive_dir.exists():
99
+ return []
100
+
101
+ repos: list[ArchivedRepo] = []
102
+ for provider_dir in sorted(archive_dir.iterdir()):
103
+ if not provider_dir.is_dir():
104
+ continue
105
+ for org_dir in sorted(provider_dir.iterdir()):
106
+ if not org_dir.is_dir():
107
+ continue
108
+ for repo_dir in sorted(org_dir.iterdir()):
109
+ if not repo_dir.is_dir():
110
+ continue
111
+ triplet = f"{provider_dir.name}/{org_dir.name}/{repo_dir.name}"
112
+ repos.append(
113
+ ArchivedRepo(
114
+ path=repo_dir,
115
+ triplet=triplet,
116
+ age_days=_age_days(repo_dir),
117
+ size_bytes=_dir_size(repo_dir),
118
+ )
119
+ )
120
+ # Oldest first
121
+ repos.sort(key=lambda r: r.age_days, reverse=True)
122
+ return repos
123
+
124
+
125
+ def prune_archived(
126
+ archive_dir: Path,
127
+ *,
128
+ older_than_days: float,
129
+ remove_all: bool = False,
130
+ dry_run: bool = False,
131
+ ) -> PruneResult:
132
+ """Remove archived repos that are older than ``older_than_days`` days.
133
+
134
+ When ``remove_all`` is True, every archived repo is removed regardless of age.
135
+ When ``dry_run`` is True, nothing is mutated — the result reports what *would* be removed
136
+ and the total bytes that would be reclaimed.
137
+
138
+ Path-escape guard: each candidate path is resolved and checked to be strictly under the
139
+ resolved ``archive_dir`` before any ``shutil.rmtree`` call. A repo path that escapes the
140
+ archive dir is silently skipped (never deleted).
141
+ """
142
+ result = PruneResult(dry_run=dry_run)
143
+ repos = list_archived(archive_dir)
144
+ resolved_root = archive_dir.resolve()
145
+
146
+ for repo in repos:
147
+ if not remove_all and repo.age_days < older_than_days:
148
+ continue
149
+
150
+ # Path-escape guard: resolve first, then check containment.
151
+ resolved_path = repo.path.resolve()
152
+ try:
153
+ resolved_path.relative_to(resolved_root)
154
+ except ValueError:
155
+ # Path escapes archive_dir — skip unconditionally.
156
+ continue
157
+
158
+ size = repo.size_bytes
159
+ result.removed.append(repo.triplet)
160
+
161
+ if not dry_run:
162
+ shutil.rmtree(resolved_path, ignore_errors=True)
163
+ result.reclaimed_bytes += size
164
+
165
+ return result
@@ -0,0 +1,10 @@
1
+ <!-- bh:agf:start (managed by `bh rig init` — edit outside these markers; `-f` refreshes) -->
2
+ ## AGF — Agentic Git Flow
3
+
4
+ This repo is onboarded as a **`bh` rig** and develops via **AGF**: work is tracked in beads
5
+ and driven through `bh`, **not** raw `git` / `bd` / `gh`.
6
+
7
+ - **Is this repo set up for AGF?** → run `bh rig ready` (add `-v` for the line-item breakdown).
8
+ - **Lifecycle, roles, conventions:** see `.beads/PRIME.md` and `docs/AGF.md`.
9
+ - Drive beads with `bh work`; load the role skill for your seat (coordinator / developer / merger).
10
+ <!-- bh:agf:end -->
@@ -0,0 +1,131 @@
1
+ # Beads + bh work — repo conventions
2
+
3
+ This repo tracks work with **bd** (beads) and integrates it with git through **`bh`**. Two
4
+ rules before anything else:
5
+
6
+ - **Drive the lifecycle with `bh work`, not raw `bd` / `git`.** `bh work` takes a bead from
7
+ assigned → merged and applies this repo's config defaults (identity, commit signing,
8
+ validation, review gate) for you.
9
+ - **Read beads with the first-class verbs** — `bh work ready|issue|list` (dependency-ordered,
10
+ byte/JSON-stable output), not raw `bd` queries.
11
+ - **File epics/molecules with `bh plan file`, never hand-create them with `bh bd create`.** The
12
+ planner compiler builds the full envelope a dispatcher needs to dispatch — the
13
+ `provider:`/`org:`/`repo:` triplet + dimension labels, the bd swarm, and a per-root kickoff
14
+ gate — which a hand-rolled `bh bd create` epic lacks.
15
+ - **`bh bd` is a gated last-resort fallback**, off by default (`passthrough.bd_enabled`): it
16
+ exits non-zero with a steering message until you set `WS_BD_PASS_ENABLED=1` (or `WS_DEBUG=1`).
17
+ Reach for it only for one-off issue surgery the convention verbs above don't cover.
18
+ - **Raw `git` is only for local work** — the actual change *inside* a worktree. Never use
19
+ raw `git` / `bd` / `gh` to drive the lifecycle (claim, submit, merge), or you bypass the
20
+ defaults the tooling sets up for you.
21
+
22
+ Beads is **issues only** — knowledge/memory lives in the project's own system (no
23
+ `bd remember`).
24
+
25
+ ## Load the skill for your role
26
+
27
+ Each role skill states its duties and the verbs it uses; all of them build on the shared
28
+ **`work`** skill (the `bh work` verb reference).
29
+
30
+ | Role | Identity | Alias | Skill | Duty |
31
+ |---|---|---|---|---|
32
+ | Dispatcher | `disp/` | overseer | `dispatcher` | deliver an epic: assign beads to developers, watch gates, re-dispatch (collapsed mode inlines the implementation) |
33
+ | Developer | `dev/` | polecat | `developer` | take one assigned bead to a reviewable state |
34
+ | Reviewer | `rev/` | — | `reviewer` | walk an approved branch, resolve or bounce the review gate |
35
+ | Merger | `merge/` | the Refinery | `merger` | serialize merges to the integration branch, preserve history |
36
+
37
+ The full seat roster (Control supervisor/director/custodian/controller, Planning planner/analyst,
38
+ Assurance warden) is in the canon `docs/design/roles-rbac-matrix.md`. Gas-Town names are optional,
39
+ non-normative aliases.
40
+
41
+ ## Conventions
42
+
43
+ - Every issue's home is the `provider:`/`org:`/`repo:` triplet — `bh plan file` injects it when
44
+ it compiles a molecule; `bh labels validate` checks it. Dependencies are declared in the
45
+ molecule spec (`deps:`) and filed by `bh plan file`, not hand-added.
46
+ - `bh plan verify <epic>` is the planner's done-gate: it checks a filed molecule against the
47
+ planning-plane conventions (bd swarm, per-root kickoff gate, triplet + closed-dimension
48
+ labels) — the same check `bh work start`/`assign`/`claim` run before dispatch.
49
+ - `bh work` reads per-rig defaults from config — load the `work` skill for details.
50
+
51
+ ### Intake + outbound state vocabulary
52
+
53
+ Cross-rig report state (epic) is modelled with native
54
+ `bd set-state <bead> <dim>=<value>` (event-sourced, with the `<dim>:<value>` label cache),
55
+ **never ad-hoc labels**. The module `beadhive/state.py` is the single owner of the closed
56
+ vocabulary — downstream beads reuse it rather than re-inventing states:
57
+
58
+ | Dimension | Value | Meaning |
59
+ |---|---|---|
60
+ | `intake` | `untriaged` | untriaged inbound — set when a report lands, **cleared on triage** |
61
+ | `intake` | `accepted` / `rejected` / `rerouted` / `promoted` | the terminal value a triage disposition transitions to (clears `untriaged`) |
62
+ | `outbound` | `pending` | staged outbound candidate — captured with **zero public exposure** |
63
+ | `publish` | `approved` | the contributor filed it upstream (behind the human publish gate) |
64
+ | `origin` | `report` \| `github` \| `import` | the intake **CHANNEL** a bead entered through — a durable, source-agnostic provenance tag (orthogonal to the `intake` *queue* state) |
65
+
66
+ These are registered as **closed dimensions** (`beadhive/state.py:STATE_DIMENSIONS`, merged in by
67
+ `registry.closed_dimensions`), so intake/outbound/origin beads validate clean under `bh labels
68
+ validate` and an unknown value (e.g. `outbound:bogus`, `origin:carrier-pigeon`) is rejected.
69
+ Queue predicates `state.is_untriaged_intake(labels)` / `state.is_promoted(labels)` /
70
+ `state.is_outbound_candidate(labels)` drive the triage, planner-adopt, and contributor queues;
71
+ `state.channel_of(labels, source_system)` (label-first, else derived from `source_system`) /
72
+ `state.origin_of(labels)` / `state.is_report_origin` resolve the intake channel.
73
+
74
+ **Provenance convention — THREE orthogonal facets (operator-approved, epic):**
75
+
76
+ 1. **System-of-record** = the **native** `source_system` + `external_ref` pair — bd's "mirrors
77
+ an external system of record" coupling, settable only at import. Reserved for **external
78
+ mirrors** (github / legacy import); a born-native report **never** overloads it.
79
+ 2. **Intake channel** = the closed `origin` dimension above (set via `bd set-state origin=…`,
80
+ like `intake`). `bh report` files via **plain `bd create` + `bd set-state origin=report`** —
81
+ this retired the old `import` + `source_system=report` workaround (follow-up).
82
+ Imported beads keep their native `source_system`; `state.origin_from_source_system(...)` derives
83
+ their channel on **read** (a uniform triage queue) **without** re-stamping an `origin` label.
84
+ 3. **Reporter identity** = `bd --actor` (unchanged) — **never** a closed label (`reported-by` is
85
+ open-ended and would fail `bh labels validate`). Do not add a reporter label dimension.
86
+
87
+ ### Fielding intake (triage duty)
88
+
89
+ Incoming reports must be **fielded, not buried in backlog**. The queue is **source-agnostic**:
90
+ `bh report` (`origin:report`), GitHub-issue import (`github`) and legacy import (`import`) all land
91
+ as `intake:untriaged` and share **one** triage queue. The **intake CHANNEL** is the closed `origin`
92
+ dimension, **not** the native `source_system`: reports carry an explicit `origin:report` label,
93
+ while imported beads derive their channel from `source_system` on read
94
+ (`state.channel_of` / `origin_from_source_system`) — uniform, no double-stamping.
95
+
96
+ - **See the queue:** `bh work intake` — this rig's untriaged intake (source-agnostic; the resolved
97
+ `origin` channel rides each row). `--source report|github|import` narrows on that channel (not raw
98
+ `source_system`). `bd find-duplicates` runs on entry (`bh report`) **and** at triage, surfacing
99
+ likely dupes so a colliding request never buries the queue.
100
+ - **See the fleet:** `bh hq intake` — the director's fleet-wide inbox (untriaged intake
101
+ across every rig).
102
+ - **Dispose (type-aware):**
103
+ - `bh work accept <id> [--type T] [--priority P]` — set type/priority, clear intake → backlog.
104
+ - `bh work reject <id> --reason "…"` — close with a reporter-visible reason.
105
+ - `bh work reroute <id> --to <rig>` — re-file a mis-routed report into the right rig; or
106
+ `--super <seat>` to bounce it to the director (stays in the fleet-wide inbox).
107
+ - `bh work promote <id>` — hand to the planner (sets `intake:promoted`, the adopt queue key;
108
+ the planner adopts it into a gated epic molecule).
109
+
110
+ - **Future follow-up:** cross-rig `bh hq` interchange (`bh plan` / `bh work --rig <id>`) is not
111
+ wired yet.
112
+
113
+ ### Escalation chain (flat-MVP)
114
+
115
+ The chain is flat and fire-and-forget at each rung:
116
+
117
+ 1. **Developer** hits a `bh` / `bd` / tool bug → `bh escalate '<what> with <tool>'` — one
118
+ one-liner to HQ; keep working. Do not route or investigate.
119
+ 2. **HQ** queues it as `intake:untriaged` with `origin:escalation`. The director sees it
120
+ via `bh hq intake` (fleet-wide inbox).
121
+ 3. **Director** is the terminal router: `bh work reroute <id> --to <rig>` re-files it
122
+ into the right rig; `bh work reroute <id> --super <seat>` keeps it in the fleet inbox for a
123
+ second look; `bh work accept/reject/promote` handle clear-cut cases.
124
+
125
+ No auto-routing exists yet. The director decides where every escalation lands.
126
+
127
+ **Dispatcher** fields intake for its own rig: `bh work intake` shows the rig queue;
128
+ `accept / reject / reroute / promote` dispose of each item. Cross-rig or ambiguous items go up
129
+ with `reroute --super`; the director picks them up from `bh hq intake`.
130
+
131
+ - Run `bd prime` after compaction or in a new session to reload this context.
@@ -0,0 +1,21 @@
1
+ {
2
+ "permissions": {
3
+ "deny": [
4
+ "Bash(bd remember:*)"
5
+ ]
6
+ },
7
+ "hooks": {
8
+ "SessionStart": [
9
+ {
10
+ "hooks": [
11
+ { "type": "command", "command": "bd prime --hook-json" }
12
+ ]
13
+ }
14
+ ]
15
+ },
16
+ "statusLine": {
17
+ "type": "command",
18
+ "command": "ws statusline",
19
+ "padding": 0
20
+ }
21
+ }