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.
- beadhive/__init__.py +1 -0
- beadhive/adopt.py +143 -0
- beadhive/archive.py +165 -0
- beadhive/assets/AGF-hint.md +10 -0
- beadhive/assets/PRIME.md +131 -0
- beadhive/assets/claude-settings.json +21 -0
- beadhive/assets/observaloop/bh-dashboard.json +1100 -0
- beadhive/assets/observaloop/cli-metrics-preset.yaml +49 -0
- beadhive/bd.py +154 -0
- beadhive/cli.py +1484 -0
- beadhive/config.py +1304 -0
- beadhive/doctor.py +675 -0
- beadhive/dolt.py +130 -0
- beadhive/escalate.py +153 -0
- beadhive/git.py +45 -0
- beadhive/gitworkspace.py +86 -0
- beadhive/guard.py +292 -0
- beadhive/hq.py +53 -0
- beadhive/hub.py +202 -0
- beadhive/identity.py +114 -0
- beadhive/log.py +176 -0
- beadhive/mcp.py +875 -0
- beadhive/metadata.py +420 -0
- beadhive/molecule.py +333 -0
- beadhive/observaloop.py +465 -0
- beadhive/observaloop_env.py +170 -0
- beadhive/onboard.py +735 -0
- beadhive/orca.py +655 -0
- beadhive/otel.py +958 -0
- beadhive/otel_lgtm.py +75 -0
- beadhive/plan.py +966 -0
- beadhive/plugins.py +64 -0
- beadhive/registry.py +425 -0
- beadhive/report.py +191 -0
- beadhive/report_target.py +120 -0
- beadhive/retire.py +368 -0
- beadhive/rig.py +658 -0
- beadhive/rig_migrate.py +167 -0
- beadhive/rig_ready.py +268 -0
- beadhive/role.py +151 -0
- beadhive/route.py +90 -0
- beadhive/run.py +78 -0
- beadhive/safety.py +1325 -0
- beadhive/schedule.py +294 -0
- beadhive/setup.py +218 -0
- beadhive/state.py +154 -0
- beadhive/survey.py +270 -0
- beadhive/templates/config.example.yaml +183 -0
- beadhive/templates/docker-compose.otel.yml +30 -0
- beadhive/templates/docker-compose.yml +24 -0
- beadhive/templates/env.example +20 -0
- beadhive/triage.py +293 -0
- beadhive/validate.py +116 -0
- beadhive/work.py +1801 -0
- beadhive/work_group.py +258 -0
- beadhive/work_logic.py +200 -0
- beadhive/work_show.py +213 -0
- beadhive/worktree.py +1516 -0
- beadhive/worktree_merge.py +238 -0
- beadhive/wt_status.py +275 -0
- beadhive-0.1.0.dist-info/METADATA +84 -0
- beadhive-0.1.0.dist-info/RECORD +64 -0
- beadhive-0.1.0.dist-info/WHEEL +4 -0
- 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 -->
|
beadhive/assets/PRIME.md
ADDED
|
@@ -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
|
+
}
|