This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit fb8e5a7b8d0f42ced642d697996e76b48036095e
Author: Stephen Mallette <[email protected]>
AuthorDate: Thu Aug 6 12:49:16 2026 -0400

    Use beads for planning and project memory
    
    .beads/PRIME.md is the canonical agent workflow: bind to a root bead, 
capture
    decisions and rejected alternatives as work happens, close-then-pin at 
merge.
    Run bin/agent-setup.sh --contributor to wire reminder hooks for Claude Code
    and Kiro.
    
    Assisted-by: Claude Code:claude-opus-5
---
 .beads/.gitignore                               |  91 ++++++++++
 .beads/PRIME.md                                 | 159 ++++++++++++++++++
 .beads/config.yaml                              |  56 ++++++
 .gitignore                                      |   5 -
 .skills/tinker-dev/SKILL.md                     |  21 ++-
 .skills/tinker-dev/references/beads-workflow.md | 215 ------------------------
 bin/agent-hooks/claude.json                     |  50 ++++++
 bin/agent-hooks/kiro.json                       |  45 +++++
 bin/agent-setup.sh                              | 107 +++++++++++-
 bin/beads-agent-hook.sh                         | 168 ++++++++++++++++++
 bin/beads-bootstrap.sh                          |  92 ----------
 bin/beads-report.py                             | 201 ++++++++++++++++++++++
 12 files changed, 888 insertions(+), 322 deletions(-)

diff --git a/.beads/.gitignore b/.beads/.gitignore
new file mode 100644
index 0000000000..9fedddedba
--- /dev/null
+++ b/.beads/.gitignore
@@ -0,0 +1,91 @@
+# Dolt database (managed by Dolt, not git)
+dolt/
+embeddeddolt/
+proxieddb/
+
+# Runtime files
+bd.sock
+bd.sock.startlock
+sync-state.json
+last-touched
+.exclusive-lock
+
+# Daemon runtime (lock, log, pid)
+daemon.*
+
+# Push state (runtime, per-machine)
+push-state.json
+
+# Lock files (various runtime locks)
+*.lock
+
+# Credential key (encryption key for federation peer auth — never commit)
+.beads-credential-key
+
+# Local version tracking (prevents upgrade notification spam after git ops)
+.local_version
+
+proxied_server_client_info.json
+
+# Worktree redirect file (contains relative path to main repo's .beads/)
+# Must not be committed as paths would be wrong in other clones
+redirect
+
+# Sync state (local-only, per-machine)
+# These files are machine-specific and should not be shared across clones
+.sync.lock
+export-state/
+export-state.json
+last_pull
+
+# Ephemeral store (SQLite - wisps/molecules, intentionally not versioned)
+ephemeral.sqlite3
+ephemeral.sqlite3-journal
+ephemeral.sqlite3-wal
+ephemeral.sqlite3-shm
+
+# Dolt server management (auto-started by bd)
+dolt-server.pid
+dolt-server.log
+dolt-server.lock
+dolt-server.port
+dolt-server.activity
+
+# Debug-mode pprof artifacts (written when dolt.debug: true in config.yaml)
+dolt-pprof/
+
+# Corrupt backup directories (created by bd doctor --fix recovery)
+*.corrupt.backup/
+
+# Backup data (auto-exported JSONL, local-only)
+backup/
+
+# Per-project environment file (Dolt connection config, GH#2520)
+.env
+
+# Legacy files (from pre-Dolt versions)
+*.db
+*.db?*
+*.db-journal
+*.db-wal
+*.db-shm
+db.sqlite
+bd.db
+# NOTE: Do NOT add negation patterns here.
+# They would override fork protection in .git/info/exclude.
+# Config files (metadata.json, config.yaml) are tracked by git by default
+# since no pattern above ignores them.
+
+# --- TinkerPop additions ---
+# metadata.json holds dolt_database, which is the LOCAL database directory name
+# (.beads/embeddeddolt/<name>/), not the DoltHub repository. The remote 
identity
+# lives in the Dolt-level `origin` remote inside that database. Clones may
+# legitimately differ, so this is per-machine and must not be shared.
+metadata.json
+
+# Generated hook shims (installed by `bd hooks install`, versioned to the bd 
binary)
+hooks/
+
+# Agent interaction audit trail — per-contributor, machine-local. Records one
+# actor's session activity, so it is neither shared history nor reproducible.
+interactions.jsonl
diff --git a/.beads/PRIME.md b/.beads/PRIME.md
new file mode 100644
index 0000000000..31b49316b0
--- /dev/null
+++ b/.beads/PRIME.md
@@ -0,0 +1,159 @@
+# TinkerPop Beads Workflow
+
+Beads is TinkerPop's planning system **and its long-term memory**. It records 
not just what
+changed, but why — decisions made, alternatives rejected, and directions 
abandoned. Treat
+every bead as something a contributor will read in three years.
+
+Full detail: the **tinker-dev** skill. This file is what must survive context 
compaction.
+
+## Core rules
+
+- **Default** — beads is the tracker for **all** work: `bd create`, `bd 
ready`, `bd close`.
+- **Prohibited** — do **not** track work in `TodoWrite`, `TaskCreate`, or a 
markdown plan
+  file. They are session-scoped: nothing in one survives, so nothing in one is 
memory. Your
+  harness may prompt you to use them. Decline.
+- **Workflow** — create the bead **before** writing code, and `--claim` it 
when you start.
+- **Plan mode** — fine, and the plan file your harness writes is not yours to 
avoid. But it
+  lives outside the repo and outside the graph. Anything you weighed and 
rejected while
+  planning belongs in a bead **before you start executing**, not after.
+
+---
+
+## 1. Start here — bind to a root
+
+Every session works under one **root bead**. Find it before writing code.
+
+```bash
+bd list --status=all --json      # filter client-side: no parent, 
open/in_progress
+bd children <root>               # recursive — the whole subtree
+```
+
+- Show the operator open/`in_progress` beads with **no parent**, most recently 
updated
+  first, and ask which one. That is your root for this session.
+- **Read `bd children <root>` before resuming work.** It is the only thing 
that makes you
+  notice a bead the work has since outgrown.
+- **If no root is selected, you are starting something new — create the root 
before writing
+  code.** Work with no bead is the failure that makes every other rule 
pointless.
+- A small fix is a lone bead. It is its own root; don't hunt for a parent.
+
+`bd query "parent=none"` does not work. Filter on the `parent` field 
client-side.
+Re-ask after a compaction rather than guessing.
+
+---
+
+## 2. While working — capture as you go
+
+**Watch for these five things. They are observable events, not judgment 
calls:**
+
+1. **The operator redirects you** — "no, do X instead", "we tried that", "that 
breaks
+   providers". Highest signal. Capture every time.
+2. **What you built diverged from the JIRA / proposal / dev@ thread.**
+3. **An approach was tried and abandoned.**
+4. **You presented options** — a decision point exists by construction.
+5. **A discovery contradicted an assumption.**
+
+**Then pick the right instrument:**
+
+| Situation | Do |
+|---|---|
+| An alternative was **actually rejected** | Create a decision bead **and** 
its rejected-alternative sibling, now |
+| Anything else worth remembering | `bd comment <root> "..."` |
+
+```bash
+bd create --type=decision --parent=<root> --title="Chose X" --design="why, and 
what X rules out"
+bd create --type=decision --parent=<root> --title="Y" 
--labels="rejected-alternative" \
+          --design="why Y was rejected"
+bd dep add <decision> <alternative> -t related
+```
+
+If nothing was rejected, it is not a decision — it is the implementation, and 
the code
+documents that. Don't inflate.
+
+An approach you tried and abandoned **is** a rejected alternative — one you 
have evidence
+for. Record it; a dead end someone already walked is worth more than a 
hypothetical.
+
+Put rationale on the root or on a decision bead. Scattered across a dozen task 
beads,
+nobody finds it.
+
+**Record what actually happened.** If you cannot point to the moment, do not 
write the bead.
+When you sense a decision you were not party to, create a bead labelled 
`human` posing the
+question instead of inventing an answer — `bd human respond <id>` turns the 
reply into a
+comment.
+
+---
+
+## 3. At merge — close, then pin
+
+When the PR lands on its target branch:
+
+```bash
+bd close <id>                    # normal completion
+bd children <root>               # the whole subtree
+bd update <id1> <id2> ... -s pinned
+bd dolt pull && bd dolt push
+```
+
+**Pin every bead in the subtree** — root, decisions, records, tasks. No 
judgment about
+which ones matter: the work shipped, so all of it is the project's history. 
Show the
+operator the list first if they want a review gate.
+
+Pinning is what makes a bead permanent — every destructive operation keys on
+`status=closed`, and pinned beads are never eligible.
+
+Push freely as a checkpoint; pinning is what marks the durable record.
+
+---
+
+## 4. Never
+
+- **Never `bd flatten`, `bd compact`, or `bd admin compact`.** They rewrite or 
discard
+  history irreversibly. `admin compact` destroys `--design` text specifically. 
`bd gc` only
+  with `--skip-decay`.
+- **Never edit an existing bead's `--design` in place.** Add a comment, or 
create a new
+  decision bead with a `supersedes` edge. Field rewrites are invisible to 
history and lose
+  the reasoning that was there.
+- `bd prune` / `bd purge` / `bd gc` are release-time maintainer operations. 
Don't run them.
+- Don't use `bd edit` — it opens `$EDITOR` and blocks.
+
+---
+
+## 5. Structure
+
+```
+root (feature/epic/task)
+  ├─relates-to──▶ record [jira]      TINKERPOP-3456
+  ├─relates-to──▶ record [pr]        apache/tinkerpop#2891
+  ├─parent-child─▶ decision  "chose X"
+  │                  └─related─▶ decision "Y" [rejected-alternative]
+  └─parent-child─▶ task      "implement X"
+```
+
+- `--parent` builds the tree; labels inherit downward, so set module/release 
labels
+  (`gremlin-core`, `3.8`) once on the root.
+- **`record` beads** hold external artifacts — JIRA, PR, dev@ thread, 
proposal. Kind is a
+  **label** (`jira`, `pr`, `dev-list`, `proposal`); the URL or ticket goes in
+  `--external-ref`. Attach them to the **root**, not to every bead. Create 
them pinned.
+  Search first — duplicates are the main risk.
+- Records are the only link between beads and code. There is no bead ID in 
commit messages.
+- Only **one dependency type per pair** — `blocks` and `discovered-from` 
cannot coexist
+  between the same two beads.
+- Never construct a bead ID; use whatever `bd create` returns. Child IDs 
encode birth
+  position (`<root>.1.2`) but do not update on reparenting — traverse `parent` 
for truth,
+  treat the ID as a hint.
+
+---
+
+## Essential commands
+
+```bash
+bd children <root>               # the subtree, recursive
+bd show <id>                     # one bead with dependencies
+bd query "status=open AND type=decision"
+bd comment <id> "..."            # append rationale (never on a task bead)
+bd create --type=... --parent=<root> --design=... --labels=...
+bd dep add <a> <b> -t related|discovered-from|supersedes
+bd update <id> --claim | -s pinned | --external-ref=TINKERPOP-NNNN
+bd search <text>
+```
+
+Priority is `0-4` (0 = critical), never "high"/"medium"/"low".
diff --git a/.beads/config.yaml b/.beads/config.yaml
new file mode 100644
index 0000000000..034e24fffc
--- /dev/null
+++ b/.beads/config.yaml
@@ -0,0 +1,56 @@
+# Beads Configuration File
+# This file configures default behavior for all bd commands in this repository
+# All settings can also be set via environment variables (BD_* prefix)
+# or overridden with command-line flags
+
+# Issue prefix for this repository (used by bd init)
+# If not set, bd init will auto-detect from directory name
+# Example: issue-prefix: "myproject" creates issues like "myproject-1", 
"myproject-2", etc.
+issue-prefix: "tp"
+
+# Use no-db mode: JSONL-only, no Dolt database
+# When true, bd will use .beads/issues.jsonl as the source of truth
+# no-db: false
+
+# Enable JSON output by default
+# json: false
+
+# Feedback title formatting for mutating commands 
(create/update/close/dep/edit)
+# 0 = hide titles, N > 0 = truncate to N characters
+# output:
+#   title-length: 255
+
+# Default actor for audit trails (overridden by BEADS_ACTOR or --actor)
+# actor: ""
+
+# Export events (audit trail) to .beads/events.jsonl on each flush/sync
+# When enabled, new events are appended incrementally using a high-water mark.
+# Use 'bd export --events' to trigger manually regardless of this setting.
+# events-export: false
+
+# Multi-repo configuration (experimental - bd-307)
+# Allows hydrating from multiple repositories and routing writes to the 
correct database
+# repos:
+#   primary: "."  # Primary repo (where this database lives)
+#   additional:   # Additional repos to hydrate from (read-only)
+#     - ~/beads-planning  # Personal planning repo
+#     - ~/work-planning   # Work planning repo
+
+# JSONL backup (periodic export for off-machine recovery)
+# Auto-enabled when a git remote exists. Override explicitly:
+# backup:
+#   enabled: false     # Disable auto-backup entirely
+#   interval: 15m      # Minimum time between auto-exports
+#   git-push: false    # Disable git push (export locally only)
+#   git-repo: ""       # Separate git repo for backups (default: project repo)
+
+# Integration settings (access with 'bd config get/set')
+# These are stored in the database, not in this file:
+# - jira.url
+# - jira.project
+# - linear.url
+# - linear.api-key
+# - github.org
+# - github.repo
+sync:
+    remote: "https://doltremoteapi.dolthub.com/tinkerpop/tinkerbeads";
diff --git a/.gitignore b/.gitignore
index beaac32145..3dd24bdc57 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,13 +52,8 @@ CLAUDE.md
 .cursor/
 .codex/
 .github/skills/
-.beads/
 gremlin-python/src/main/python/build/
 node_modules/
 node/
 gremlin-js/node_modules/
 gremlin-js/node/
-.dolt/
-*.db
-state.json
-.beads-credential-key
diff --git a/.skills/tinker-dev/SKILL.md b/.skills/tinker-dev/SKILL.md
index 236813b49b..073473c552 100644
--- a/.skills/tinker-dev/SKILL.md
+++ b/.skills/tinker-dev/SKILL.md
@@ -151,13 +151,22 @@ Otherwise, match the existing code in neighboring files — 
explicit imports (no
 
 ## Beads Caveats
 
-The general agent Do/Don't rules are in the root `AGENTS.md`. Two beads rules 
are easy to get
-wrong and worth repeating here (full workflow in 
`references/beads-workflow.md`):
+The general agent Do/Don't rules are in the root `AGENTS.md`. **Run `bd prime` 
at the start of
+a session and again after any context compaction** — it emits 
`.beads/PRIME.md`, the canonical
+workflow. If `bd` is not installed, skip it; nothing else in this skill 
depends on beads.
 
-- Don't run `bd dolt push` — pushing to DoltHub is a maintainer action 
performed after PRs
-  merge, not during active development.
+Four rules are easy to get wrong and worth repeating here:
+
+- Beads is the tracker for all work. Don't keep the plan in `TodoWrite`, 
`TaskCreate`, or a
+  markdown file — those are session-scoped, so nothing tracked there becomes 
memory. Create
+  the bead before writing code.
 - Don't close a beads issue when a PR is submitted — close it only after the 
PR merges to the
-  target branch.
+  target branch, then **pin** every bead in the subtree: root, decisions, 
records and tasks.
+  Pinning is what makes a bead permanent; everything destructive keys on 
`status=closed`.
+- Never run `bd flatten`, `bd compact`, or `bd admin compact`. They rewrite or 
discard history
+  irreversibly, and `admin compact` destroys `--design` text specifically.
+- Put rationale on the root or on a decision bead, not spread across task 
beads — scattered
+  that way, nobody finds it.
 
 ## Reference Guides
 
@@ -166,4 +175,4 @@ task-specific guidance, see:
 
 - [Development Environment Setup](references/dev-environment-setup.md) — fresh 
clone to working environment, prerequisites, GLV activation
 - [Gremlin MCP Server](references/gremlin-mcp.md) — translation, formatting, 
querying via MCP
-- [Beads Workflow](references/beads-workflow.md) — agent planning, persistent 
memory, TinkerPop-specific conventions and DoltHub push policy
+- Beads workflow — run `bd prime` (emits `.beads/PRIME.md`): root binding, 
decision capture, pin-at-merge, structure conventions
diff --git a/.skills/tinker-dev/references/beads-workflow.md 
b/.skills/tinker-dev/references/beads-workflow.md
deleted file mode 100644
index ed03954946..0000000000
--- a/.skills/tinker-dev/references/beads-workflow.md
+++ /dev/null
@@ -1,215 +0,0 @@
-# Beads Workflow for TinkerPop
-
-TinkerPop uses [beads](https://github.com/steveyegge/beads) (`bd`) as the 
agent planning,
-execution, and long-term memory system, backed by a shared DoltHub database
-(`tinkerpop/tinkerbeads`). This reference covers TinkerPop-specific 
conventions that differ
-from or extend the beads defaults.
-
-## Setup
-
-New contributors run the bootstrap script from the repo root:
-
-```bash
-bin/beads-bootstrap.sh
-```
-
-Prerequisites: `bd` and `dolt` installed, valid DoltHub credentials (`dolt 
login`).
-
-## Core Rules
-
-- **Default**: Use beads for ALL planning and tracking (`bd create`, `bd 
ready`, `bd close`)
-- **Scope**: Beads captures both concrete tasks *and* open design questions — 
create a bead
-  whenever a question, decision, or trade-off needs to be preserved, not just 
when there is
-  code to write
-- **Prohibited**: Do NOT use TodoWrite, TaskCreate, or markdown files for task 
tracking
-- **Workflow**: Create a beads issue BEFORE writing code, mark `in_progress` 
when starting
-- **Commit messages**: Every commit must include bead ID(s) as 
`(tinkerpop-NNN)` on its own
-  line, placed above the `Assisted-by:` trailer. Validate this before 
committing:
-  ```
-  Add vertex label support to GraphSON serializer
-
-  (tinkerpop-123)
-  Assisted-by: Claude:claude-sonnet-4-6 [Claude Code]
-  ```
-- **Bias toward persistence**: When in doubt, capture it in a bead. Lost 
reasoning is harder
-  to recover than an unused issue.
-- **Session start**: Check `bd ready` for available work before beginning any 
session
-
-## Essential Commands
-
-### Finding Work
-- `bd ready` - Show issues ready to work (no blockers)
-- `bd list --status=open` - All open issues
-- `bd list --status=in_progress` - Your active work
-- `bd show <id>` - Detailed issue view with dependencies
-
-### Creating & Updating
-- `bd create --title="Summary of this issue" --description="Why this issue 
exists and what needs to be done" --type=task|bug|feature --priority=2` - New 
issue
-  - Priority: 0-4 or P0-P4 (0=critical, 2=medium, 4=backlog). NOT 
"high"/"medium"/"low"
-- `bd update <id> --claim` - Claim work
-- `bd update <id> --assignee=username` - Assign to someone
-- `bd update <id> --title/--description/--notes/--design` - Update fields 
inline
-- `bd close <id>` - Mark complete
-- `bd close <id1> <id2> ...` - Close multiple issues at once (more efficient)
-- `bd close <id> --reason="explanation"` - Close with reason
-- **Tip**: When creating multiple issues/tasks/epics, use parallel subagents 
for efficiency
-- **WARNING**: Do NOT use `bd edit` - it opens $EDITOR (vim/nano) which blocks 
agents
-
-### Dependencies & Blocking
-- `bd dep add <issue> <depends-on>` - Add dependency (issue depends on 
depends-on)
-- `bd blocked` - Show all blocked issues
-- `bd show <id>` - See what's blocking/blocked by this issue
-
-### Sync & Collaboration
-- **WARNING**: Do NOT use `bd dolt push` - that is a manual task left to human 
maintainers
-- `bd dolt pull` - Pull beads from Dolt remote
-- `bd search <query>` - Search issues by keyword
-
-### Project Health
-- `bd stats` - Project statistics (open/closed/blocked counts)
-- `bd doctor` - Check for issues (sync problems, missing hooks)
-- `bd doctor --check=conventions` - Check for convention drift (lint, stale, 
orphans)
-
-### Quality Tools
-- `bd create --validate` - Check description has required sections
-- `bd create --acceptance="criteria"` - Set acceptance criteria (checked by 
--validate)
-- `bd create --design="decisions"` - Record design decisions
-- `bd create --notes="context"` - Add supplementary notes
-- `bd config set validation.on-create warn` - Auto-validate on every create
-- `bd lint` - Check existing issues for missing sections
-
-### Lifecycle & Hygiene
-- `bd defer <id> --until="date"` - Defer work to a future date
-- `bd supersede <id> --with=<new-id>` - Mark issue as superseded
-- `bd close <id> --suggest-next` - Show newly unblocked issues after closing
-- `bd stale` - Find issues with no recent activity
-- `bd orphans` - Find issues with broken dependencies
-- `bd preflight` - Pre-PR checks (lint, stale, orphans)
-- `bd human <id>` - Flag for human decision (list/respond/dismiss)
-
-### Structured Workflows
-- `bd formula list` - See available workflow templates
-- `bd mol pour <name>` - Start structured workflow from formula
-
-## Labels
-
-Labels provide multi-dimensional categorization orthogonal to type and 
priority. An issue
-can carry multiple labels simultaneously, enabling cross-cutting views of the 
issue graph.
-
-```bash
-# At creation
-bd create --title="..." --labels="gremlin-core,3.8"
-
-# After creation
-bd label add <id> <label>
-bd label remove <id> <label>
-bd label list <id>          # labels on a specific issue
-bd label list-all           # all labels in use across the database
-```
-
-Suggested label dimensions for TinkerPop:
-- **Module**: `gremlin-core`, `gremlin-server`, `gremlin-python`, 
`gremlin-javascript`, `gremlin-dotnet`, `gremlin-go`, `tinkergraph`
-- **Cross-cutting concern**: `serialization`, `traversal`, `driver`, `docs`, 
`breaking-change`, `deprecation`
-
-These are conventions, not enforced values. Use `bd label list-all` to see 
what labels are
-already in use before introducing new ones.
-
-## Issue Lifecycle
-
-```
-open → in_progress → closed
-```
-
-Close a bead when its associated commit has been made and the operator has 
approved the
-commit message. The bead represents completed local work; the DoltHub push 
(which makes
-it visible to others) is a separate, deferred maintainer action.
-
-## Issue Content Standards
-
-Every issue must carry enough context to be understood without a side 
conversation.
-Use these fields when creating:
-
-```bash
-bd create \
-  --title="Short imperative summary" \
-  --description="What the problem is and why it matters" \
-  --design="Decisions made, alternatives considered, approaches rejected" \
-  --acceptance="Testable definition of done" \
-  --labels="module,release-target" \
-  --type=bug|feature|task \
-  --priority=0-4
-```
-
-- **`--description`**: The *why*, not just the *what*. A reader should 
understand the
-  motivation without prior context. For open design questions, state what is 
unresolved
-  and why it matters.
-- **`--design`**: Record decisions made, alternatives considered, and 
approaches rejected.
-  This is where the reasoning lives — future sessions and agents read this to 
understand
-  the path that led to the current state, not just what the current state is.
-- **`--acceptance`**: What does done look like? Should be verifiable (test 
passes, behavior
-  observed, doc updated). For open questions, the condition under which the 
bead can close.
-
-Use `bd create --validate` to check completeness before finalizing.
-
-## JIRA Linking
-
-When a TinkerPop JIRA ticket exists for a bead, record it via 
`--external-ref`. This can
-be set at creation time or added later when the operator supplies the ticket 
identifier:
-
-```bash
-# At creation
-bd create --title="..." --external-ref="TINKERPOP-3456" ...
-
-# Added later
-bd update <id> --external-ref="TINKERPOP-3456"
-```
-
-## Daily Workflow
-
-**Finding and claiming work:**
-```bash
-bd ready                    # show unblocked issues
-bd show <id>                # review details
-bd update <id> --claim      # claim it
-```
-
-**Completing work:**
-```bash
-# 1. Ensure commit message includes the bead ID above the Assisted-by trailer
-# 2. Get operator approval on the commit message
-# 3. Close the bead
-bd close <id>
-```
-
-**Checking project state:**
-```bash
-bd stats                    # open/closed/blocked counts
-bd blocked                  # issues with unresolved blockers
-bd list --status=in_progress  # all active work
-```
-
-**Creating dependent work:**
-```bash
-# Run bd create commands in parallel (use subagents for many items)
-bd create --title="Implement feature X" --description="Why this issue exists 
and what needs to be done" --type=feature
-bd create --title="Write tests for X" --description="Why this issue exists and 
what needs to be done" --type=task
-bd dep add beads-yyy beads-xxx  # Tests depend on Feature (Feature blocks 
tests)
-```
-
-## DoltHub Push Policy
-
-**Contributors do not push to DoltHub.** `bd dolt push` is a maintainer action 
performed
-after PRs merge. Beads are closed locally as work completes; the push to 
DoltHub defers
-visibility of that closed state until the work has been reviewed and merged.
-
-Maintainers run after merging a batch of PRs:
-```bash
-bd dolt pull   # integrate any remote changes first
-bd dolt push   # publish to tinkerpop/tinkerbeads
-```
-
-## What Not To Do
-
-- Do not run `bd dolt push` as a contributor — this is a maintainer action 
post-merge
-- Do not use `bd edit` — it opens an interactive editor that blocks agents
-- Do not serialize issue creation or task execution when parallelization is 
possible
diff --git a/bin/agent-hooks/claude.json b/bin/agent-hooks/claude.json
new file mode 100644
index 0000000000..9a7230e7ce
--- /dev/null
+++ b/bin/agent-hooks/claude.json
@@ -0,0 +1,50 @@
+{
+  "_comment": [
+    "Claude Code hook wiring for TinkerPop's beads workflow. Merged into",
+    ".claude/settings.local.json by 'bin/agent-setup.sh --contributor 
claude'.",
+    "All logic lives in bin/beads-agent-hook.sh -- these entries only name an",
+    "event and invoke it. Entries are identified for re-install and removal 
by",
+    "the beads-agent-hook.sh reference in their command.",
+    "SessionStart matches 'compact' as well as startup/resume/clear, so the",
+    "workflow is restored after a context compaction, not just at session 
start.",
+    "There is deliberately no PreCompact entry: that event accepts only",
+    "decision/reason, so it has no way to inject context. A reminder issued 
there",
+    "cannot reach the model. SessionStart with source=compact does the real 
work."
+  ],
+  "hooks": {
+    "SessionStart": [
+      {
+        "matcher": "startup|resume|clear|compact",
+        "hooks": [
+          {
+            "type": "command",
+            "command": "\"${CLAUDE_PROJECT_DIR}\"/bin/beads-agent-hook.sh 
session-start --format=claude",
+            "timeout": 30
+          }
+        ]
+      }
+    ],
+    "Stop": [
+      {
+        "hooks": [
+          {
+            "type": "command",
+            "command": "\"${CLAUDE_PROJECT_DIR}\"/bin/beads-agent-hook.sh stop 
--format=claude",
+            "timeout": 15
+          }
+        ]
+      }
+    ],
+    "UserPromptSubmit": [
+      {
+        "hooks": [
+          {
+            "type": "command",
+            "command": "\"${CLAUDE_PROJECT_DIR}\"/bin/beads-agent-hook.sh 
prompt-submit --format=claude",
+            "timeout": 15
+          }
+        ]
+      }
+    ]
+  }
+}
diff --git a/bin/agent-hooks/kiro.json b/bin/agent-hooks/kiro.json
new file mode 100644
index 0000000000..d71877351a
--- /dev/null
+++ b/bin/agent-hooks/kiro.json
@@ -0,0 +1,45 @@
+{
+  "version": "v1",
+  "_comment": [
+    "Kiro hook wiring for TinkerPop's beads workflow. Copied to",
+    ".kiro/hooks/tinkerpop-beads.json by 'bin/agent-setup.sh --contributor 
kiro'.",
+    "All logic lives in bin/beads-agent-hook.sh -- these entries only name an",
+    "event and invoke it. Names are prefixed tinkerpop-beads- for re-install.",
+    "Kiro has no PreCompact trigger, but neither harness can inject context 
at",
+    "compaction time, so nothing is lost relative to Claude. Whether Kiro 
re-fires",
+    "SessionStart after compacting is untested; if it does not, the workflow 
is not",
+    "restored and the agent should be told to run 'bd prime' by hand."
+  ],
+  "hooks": [
+    {
+      "name": "tinkerpop-beads-session-start",
+      "trigger": "SessionStart",
+      "action": {
+        "type": "agent",
+        "prompt": "Run `bin/beads-agent-hook.sh session-start` and follow the 
workflow it prints. It is TinkerPop's beads workflow: how to bind to a root 
bead, when to capture decisions, and what never to run. If the command is 
unavailable, continue without it."
+      },
+      "timeout": 30,
+      "enabled": true
+    },
+    {
+      "name": "tinkerpop-beads-stop",
+      "trigger": "Stop",
+      "action": {
+        "type": "agent",
+        "prompt": "Run `bin/beads-agent-hook.sh stop`. If it prints nothing, 
say nothing and stop. If it prints a check, act on it before finishing."
+      },
+      "timeout": 15,
+      "enabled": true
+    },
+    {
+      "name": "tinkerpop-beads-prompt-submit",
+      "trigger": "UserPromptSubmit",
+      "action": {
+        "type": "agent",
+        "prompt": "If the operator just redirected you -- rejected an 
approach, said an alternative was already tried, or told you to do something a 
different way -- create a decision bead and its rejected-alternative sibling 
now, while the reasoning is exact. Otherwise ignore this and carry on."
+      },
+      "timeout": 15,
+      "enabled": true
+    }
+  ]
+}
diff --git a/bin/agent-setup.sh b/bin/agent-setup.sh
index 4b817ec585..f16cef3fd0 100755
--- a/bin/agent-setup.sh
+++ b/bin/agent-setup.sh
@@ -60,14 +60,15 @@ skip() { echo -e "  ${YELLOW}○${NC} $1"; }
 bad()  { echo -e "  ${RED}✗${NC} $1"; }
 
 usage() {
-    echo "Usage: bin/agent-setup.sh <agent|--list|--all>"
+    echo "Usage: bin/agent-setup.sh <agent|--list|--all|--contributor [agent]>"
     echo ""
     echo "Agents: claude, copilot, cursor, codex, junie, kiro"
     echo ""
     echo "Options:"
-    echo "  --list    List supported agents and their skill discovery paths"
-    echo "  --all     Set up shims for all supported agents"
-    echo "  --help    Show this message"
+    echo "  --list           List supported agents and their skill discovery 
paths"
+    echo "  --all            Set up shims for all supported agents"
+    echo "  --contributor    Also install beads workflow hooks (committers; 
claude, kiro)"
+    echo "  --help           Show this message"
 }
 
 # Verify we're in the repo root
@@ -197,6 +198,85 @@ setup_agent() {
     esac
 }
 
+# --- Contributor hooks (opt-in) ---------------------------------------------
+#
+# Beads is a committer tool, so hook wiring is opt-in via --contributor. The
+# logic lives in bin/beads-agent-hook.sh; the JSON under bin/agent-hooks/ only
+# names events and invokes it. Installs are idempotent: our entries are found
+# by their beads-agent-hook.sh reference (claude) or tinkerpop-beads- name
+# prefix (kiro), removed, then rewritten.
+
+HOOK_AGENTS=("claude" "kiro")
+
+setup_claude_hooks() {
+    local settings=".claude/settings.local.json"
+    mkdir -p ".claude"
+    [[ -f "$settings" ]] || echo '{}' > "$settings"
+
+    if ! python3 - "$settings" "bin/agent-hooks/claude.json" <<'PY'
+import json, sys
+
+settings_path, hooks_path = sys.argv[1], sys.argv[2]
+with open(settings_path) as fh:
+    settings = json.load(fh)
+with open(hooks_path) as fh:
+    ours = {k: v for k, v in json.load(fh)["hooks"].items()}
+
+MARKER = "beads-agent-hook.sh"
+
+
+def is_ours(entry):
+    return any(MARKER in h.get("command", "") for h in entry.get("hooks", []))
+
+
+# Sweep every event, not just the ones we are about to write: an event we no
+# longer wire (PreCompact, once) must not be orphaned in the user's settings.
+existing = settings.setdefault("hooks", {})
+for event in list(existing):
+    kept = [e for e in existing[event] if not is_ours(e)]
+    if kept:
+        existing[event] = kept
+    else:
+        del existing[event]
+
+for event, entries in ours.items():
+    existing[event] = existing.get(event, []) + entries
+
+with open(settings_path, "w") as fh:
+    json.dump(settings, fh, indent=2)
+    fh.write("\n")
+PY
+    then
+        bad "claude: could not merge hooks into $settings"
+        return 1
+    fi
+    ok "claude: merged beads hooks into $settings (SessionStart, Stop, 
UserPromptSubmit)"
+}
+
+setup_kiro_hooks() {
+    mkdir -p ".kiro/hooks"
+    cp "bin/agent-hooks/kiro.json" ".kiro/hooks/tinkerpop-beads.json"
+    ok "kiro: wrote .kiro/hooks/tinkerpop-beads.json (SessionStart, Stop, 
UserPromptSubmit)"
+}
+
+setup_hooks() {
+    local agent="$1"
+
+    if ! command -v bd >/dev/null 2>&1; then
+        skip "$agent: 'bd' not found — hooks installed anyway, they stay 
silent without it"
+    fi
+
+    case "$agent" in
+        claude) setup_claude_hooks ;;
+        kiro)   setup_kiro_hooks ;;
+        *)
+            bad "No hook support for: $agent"
+            echo "  Hooks are available for: ${HOOK_AGENTS[*]}"
+            return 1
+            ;;
+    esac
+}
+
 list_agents() {
     echo "Supported agents and their skill discovery paths:"
     echo ""
@@ -235,6 +315,25 @@ case "$1" in
         echo "Done. Symlinked directories and generated files are gitignored."
         echo "Add them to .gitignore if they aren't already."
         ;;
+    --contributor)
+        shift
+        if [[ $# -eq 0 ]]; then
+            echo "Setting up beads hooks for all supported agents..."
+            echo ""
+            for agent in "${HOOK_AGENTS[@]}"; do
+                setup_hooks "$agent"
+            done
+        else
+            echo "Setting up beads hooks for $1..."
+            echo ""
+            setup_hooks "$1"
+        fi
+        echo ""
+        echo "Hooks are advisory — they remind, they never block. Logic lives 
in"
+        echo "bin/beads-agent-hook.sh; run it directly to see what an agent is 
shown:"
+        echo ""
+        echo "  bin/beads-agent-hook.sh stop"
+        ;;
     *)
         echo "Setting up $1..."
         echo ""
diff --git a/bin/beads-agent-hook.sh b/bin/beads-agent-hook.sh
new file mode 100755
index 0000000000..3cd9e6a8c0
--- /dev/null
+++ b/bin/beads-agent-hook.sh
@@ -0,0 +1,168 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# Agent hook logic for TinkerPop's beads workflow.
+#
+# All of the behaviour lives here so it can be read, reviewed and tested 
without
+# a running agent. The per-tool files under bin/agent-hooks/ are thin wiring 
that
+# name an event and invoke this script; bin/agent-setup.sh --contributor 
installs
+# them. Every hook is advisory -- nothing here blocks an agent.
+#
+# Usage:
+#   bin/beads-agent-hook.sh <event> [--format=claude|plain]
+#
+# Events:
+#   session-start   emit the beads workflow (.beads/PRIME.md, via bd prime)
+#   stop            wrap-up checklist, rate limited (see STOP_INTERVAL)
+#   prompt-submit   nudge when the operator's prompt reads as a redirect
+#
+# Run any event by hand to see exactly what an agent would be shown:
+#   bin/beads-agent-hook.sh stop
+#
+set -uo pipefail
+
+STOP_INTERVAL=${TINKERPOP_BEADS_STOP_INTERVAL:-1800}  # seconds; 0 disables 
limit
+
+usage() {
+    awk '/^# Agent hook logic/,/^[^#]/ { if ($0 ~ /^#/) { sub(/^# ?/, ""); 
print } }' "$0"
+    exit "${1:-0}"
+}
+
+event=""
+format="plain"
+for arg in "$@"; do
+    case "$arg" in
+        --format=*) format="${arg#--format=}" ;;
+        -h|--help)  usage 0 ;;
+        -*)         echo "unknown option: $arg" >&2; usage 2 ;;
+        *)          event="$arg" ;;
+    esac
+done
+[[ -n "$event" ]] || usage 2
+
+# Beads is for committers. A contributor without bd installed, or a directory
+# that is not a beads workspace, gets silence rather than an error.
+command -v bd >/dev/null 2>&1 || exit 0
+bd where >/dev/null 2>&1 || exit 0
+
+# Read the hook payload when one is piped in. Agents send JSON on stdin; 
running
+# this by hand from a terminal must not block waiting for input.
+read_stdin_payload() {
+    [[ -t 0 ]] && return 0
+    cat 2>/dev/null
+}
+
+# Pull a field out of the agent's JSON payload, tolerating non-JSON input.
+payload_field() {
+    local payload="$1" field="$2"
+    printf '%s' "$payload" | python3 -c "
+import json, sys
+raw = sys.stdin.read()
+try:
+    print(json.loads(raw).get('$field', '') or '')
+except Exception:
+    print(raw if '$field' == 'user_prompt' else '')
+" 2>/dev/null
+}
+
+# Stop fires after every agent turn, so an unconditional checklist would be
+# noise. Fire at most once per STOP_INTERVAL per workspace. State lives in the
+# temp dir rather than the repo so nothing is left behind to commit.
+stop_is_due() {
+    [[ "$STOP_INTERVAL" -eq 0 ]] && return 0
+    local key stamp now last
+    key=$(printf '%s' "$PWD" | cksum | cut -d' ' -f1)
+    stamp="${TMPDIR:-/tmp}/tinkerpop-beads-stop-$key"
+    now=$(date +%s)
+    last=$(cat "$stamp" 2>/dev/null || echo 0)
+    (( now - last < STOP_INTERVAL )) && return 1
+    echo "$now" > "$stamp" 2>/dev/null
+    return 0
+}
+
+# High-signal redirect language. The cost of a false positive is one extra
+# sentence in context, so this errs toward firing.
+REDIRECT_RE='(^|[[:space:]])(no,|nope|instead|rather than|we tried|already 
tried|'\
+'that will not work|that won.t work|that breaks|don.t do|do not do|revert|back 
out|'\
+'undo that|wrong approach|not what i)([[:space:]]|[[:punct:]]|$)'
+
+text=""
+case "$event" in
+    session-start)
+        text=$(bd prime 2>/dev/null)
+        ;;
+
+    stop)
+        stop_is_due || exit 0
+        # bd prints "No issues found." rather than nothing when the list is 
empty.
+        active=$(bd list --status=in_progress 2>/dev/null | grep -v '^No 
issues found' | head -20)
+        text="Beads check — did anything get decided this session?
+
+An alternative that was actually considered and rejected belongs in a decision 
bead with
+its rejected-alternative sibling. An approach you tried and abandoned counts. 
Anything else
+worth remembering goes in a comment on the root bead."
+        if [[ -n "$active" ]]; then
+            text="$text
+
+Still in progress:
+$active"
+        fi
+        ;;
+
+    prompt-submit)
+        prompt=$(payload_field "$(read_stdin_payload)" user_prompt)
+        [[ -n "$prompt" ]] || exit 0
+        if printf '%s' "$prompt" | tr '[:upper:]' '[:lower:]' | grep -Eq 
"$REDIRECT_RE"; then
+            text="That prompt reads as a redirect. If an alternative was just 
rejected, create the
+decision bead and its rejected-alternative sibling now, while the reasoning is 
exact —
+operator redirects are the highest-signal capture trigger there is."
+        fi
+        ;;
+
+    *)
+        echo "unknown event: $event" >&2
+        usage 2
+        ;;
+esac
+
+[[ -n "${text// /}" ]] || exit 0
+
+case "$format" in
+    plain)
+        printf '%s\n' "$text"
+        ;;
+    claude)
+        case "$event" in
+            session-start)  hook_event="SessionStart" ;;
+            stop)           hook_event="Stop" ;;
+            prompt-submit)  hook_event="UserPromptSubmit" ;;
+        esac
+        HOOK_EVENT="$hook_event" HOOK_TEXT="$text" python3 -c "
+import json, os
+print(json.dumps({'hookSpecificOutput': {
+    'hookEventName': os.environ['HOOK_EVENT'],
+    'additionalContext': os.environ['HOOK_TEXT'],
+}}))
+"
+        ;;
+    *)
+        echo "unknown format: $format" >&2
+        exit 2
+        ;;
+esac
diff --git a/bin/beads-bootstrap.sh b/bin/beads-bootstrap.sh
deleted file mode 100755
index ad0980a224..0000000000
--- a/bin/beads-bootstrap.sh
+++ /dev/null
@@ -1,92 +0,0 @@
-#!/bin/bash
-#
-#
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-#
-
-# This script helps automate the Beads process setup for TinkerPop.
-# It initializes Beads and clones the tinkerpop/tinkerbeads database.
-# Contributors can use this to quickly set up their local environment.
-
-set -e
-
-# Check if bd command exists
-if ! command -v bd &> /dev/null; then
-    echo "Error: 'bd' command not found. Please install Beads first: 
https://github.com/steveyegge/beads";
-    exit 1
-fi
-
-# Check if dolt command exists
-if ! command -v dolt &> /dev/null; then
-    echo "Error: 'dolt' command not found. Please install Dolt first: 
https://www.dolthub.com/docs/introduction/installation/";
-    exit 1
-fi
-
-# Check if dolt credentials are valid
-echo "Checking Dolt credentials..."
-if ! dolt creds check; then
-    echo "Error: Dolt credentials check failed."
-    echo "Please run 'dolt login' to authenticate with DoltHub."
-    exit 1
-fi
-
-# Ensure we are in the project root
-if [ ! -f "pom.xml" ]; then
-    echo "Error: This script must be run from the TinkerPop project root."
-    exit 1
-fi
-
-echo "Initializing Beads..."
-# Run bd init if .beads directory doesn't exist
-if [ ! -d ".beads" ]; then
-    bd init --skip-agents --skip-hooks --database tinkerbeads --prefix 
tinkerpop
-else
-    echo ".beads directory already exists. Skipping 'bd init'."
-fi
-
-echo "Setting up Dolt database..."
-# Navigate to the embedded dolt directory
-mkdir -p .beads/embeddeddolt
-pushd .beads/embeddeddolt > /dev/null
-
-# Remove existing database if any, to allow cloning
-if [ -d "tinkerbeads" ]; then
-    echo "Removing existing local tinkerbeads database..."
-    rm -rf tinkerbeads
-fi
-
-echo "Cloning tinkerpop/tinkerbeads from DoltHub..."
-dolt clone tinkerpop/tinkerbeads
-
-popd > /dev/null
-
-echo "Removing project_id from metadata.json to avoid identity mismatch..."
-if [ -f ".beads/metadata.json" ]; then
-    # Use jq to safely remove the project_id field.
-    if command -v jq &> /dev/null; then
-        jq 'del(.project_id)' .beads/metadata.json > .beads/metadata.json.tmp 
&& mv .beads/metadata.json.tmp .beads/metadata.json
-    else
-        # Fallback to sed if jq is not available
-        sed -i '/"project_id":/d' .beads/metadata.json
-    fi
-fi
-
-echo "Finalizing setup..."
-bd ready
-
-echo "Beads bootstrap complete for TinkerPop!"
diff --git a/bin/beads-report.py b/bin/beads-report.py
new file mode 100755
index 0000000000..466c937936
--- /dev/null
+++ b/bin/beads-report.py
@@ -0,0 +1,201 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+"""Structural report over the beads graph.
+
+ADVISORY ONLY. This never blocks anything. Run as a gate it would fire 
constantly
+on legitimate work -- a one-line fix has no record bead and no decisions, and 
that
+is correct.
+
+Two scopes:
+
+  bin/beads-report.py --root tp-abc   At merge, before pinning. Reads just that
+                                      subtree via `bd children`. Bounded and 
fast.
+
+  bin/beads-report.py                 At release, before purging. Reads the 
whole
+                                      database via `bd export`. Its most 
valuable
+                                      output is "here is every piece of 
rationale
+                                      about to be deleted" -- review it, pin 
what
+                                      should survive, then purge.
+
+Checks divide into two kinds, and the distinction matters:
+
+  OBJECTIVE   dangling edge targets; rationale on closed unpinned beads.
+              Defects and facts. No judgment involved.
+
+  HEURISTIC   decision beads with no rejected-alternative neighbour; roots with
+              several tasks and no decisions. These cannot distinguish "no
+              decisions were made" from "decisions were not captured", so they
+              are questions for a human, never verdicts.
+
+Neither kind can judge whether design text is real reasoning or fluent filler.
+Structure is checkable; substance is not.
+"""
+
+import argparse
+import collections
+import json
+import subprocess
+import sys
+
+
+def bd(*args):
+    """Run a bd command with --json and return the parsed result."""
+    proc = subprocess.run(["bd", *args, "--json"], capture_output=True, 
text=True)
+    if proc.returncode != 0:
+        sys.exit(f"bd {' '.join(args)} failed: {proc.stderr.strip()}")
+    if not proc.stdout.strip():
+        return []
+    return json.loads(proc.stdout)
+
+
+def as_list(payload):
+    """bd sometimes returns a bare object where a list is expected."""
+    if isinstance(payload, dict):
+        return payload.get("issues", [payload])
+    return payload
+
+
+def load_export():
+    """Whole database, one record per line. Carries design and comments."""
+    proc = subprocess.run(["bd", "export"], capture_output=True, text=True)
+    if proc.returncode != 0:
+        sys.exit(f"bd export failed: {proc.stderr.strip()}")
+    beads = {}
+    for line in proc.stdout.splitlines():
+        line = line.strip()
+        if not line:
+            continue
+        record = json.loads(line)
+        if record.get("_type") == "issue":
+            beads[record["id"]] = record
+    return beads
+
+
+def load_subtree(root):
+    """One root's subtree. `bd children` recurses, but the records it returns
+    carry no `design` field -- only comment_count. Rationale-at-risk detection
+    is correspondingly weaker in this scope."""
+    beads = {b["id"]: b for b in as_list(bd("children", root))}
+    for record in as_list(bd("show", root)):
+        beads.setdefault(record["id"], record)
+    return beads
+
+
+def edges_of(beads):
+    return [
+        (bid, dep["depends_on_id"], dep["type"])
+        for bid, bead in beads.items()
+        for dep in bead.get("dependencies", [])
+    ]
+
+
+def report(title, rows):
+    print(f"\n{title}")
+    print("  (none)" if not rows else "\n".join(f"  {r}" for r in rows))
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
+    parser.add_argument("--root", help="limit to one root's subtree (merge 
scope)")
+    args = parser.parse_args()
+
+    scoped = bool(args.root)
+    beads = load_subtree(args.root) if scoped else load_export()
+    edges = edges_of(beads)
+
+    neighbours = collections.defaultdict(set)
+    for src, dst, _ in edges:
+        neighbours[src].add(dst)
+        neighbours[dst].add(src)
+
+    # --- OBJECTIVE ---------------------------------------------------------
+
+    # Dangling targets are only meaningful against the whole graph; in a 
subtree
+    # a "missing" target is usually just outside the scope.
+    if not scoped:
+        report("Dangling dependency targets", [
+            f"{s} -[{t}]-> {d}  (target missing)"
+            for s, d, t in edges if d not in beads
+        ])
+
+    at_risk = []
+    for bead in beads.values():
+        if bead.get("status") != "closed":
+            continue  # pinned and open beads are not purge-eligible
+        carried = []
+        if bead.get("comment_count") or bead.get("comments"):
+            n = bead.get("comment_count") or len(bead.get("comments") or [])
+            carried.append(f"{n} comment(s)")
+        if (bead.get("design") or "").strip():
+            carried.append("design text")
+        if carried:
+            at_risk.append(f"{bead['id']:18} {', '.join(carried):22} 
{bead['title'][:44]}")
+    report("Rationale on closed beads (lost at next purge -- pin to keep)", 
at_risk)
+    if scoped:
+        print("  note: subtree scope cannot see `design` text; comment counts 
only")
+
+    # --- HEURISTIC ---------------------------------------------------------
+
+    lonely = [
+        f"{b['id']:18} {b['title'][:50]}"
+        for b in beads.values()
+        if b.get("issue_type") == "decision"
+        and not any("rejected-alternative" in (beads[n].get("labels") or [])
+                    for n in neighbours[b["id"]] if n in beads)
+    ]
+    report("Decision beads with no rejected-alternative neighbour (suspect)", 
lonely)
+
+    # `bd children --json` carries a `parent` field; `bd export` does not -- 
there
+    # the hierarchy lives only in parent-child edges, which point child -> 
parent.
+    parent_of = {b: bead["parent"] for b, bead in beads.items() if 
bead.get("parent")}
+    for src, dst, typ in edges:
+        if typ == "parent-child":
+            parent_of.setdefault(src, dst)
+
+    children = collections.defaultdict(list)
+    for bid, parent in parent_of.items():
+        children[parent].append(beads[bid])
+
+    roots = [args.root] if scoped else [
+        b for b in beads if b not in parent_of and children.get(b)
+    ]
+
+    thin = []
+    for root in roots:
+        if root not in beads:
+            continue
+        kids = children.get(root, [])
+        kinds = collections.Counter(k.get("issue_type") for k in kids)
+        linked = [beads[n] for n in neighbours[root] if n in beads]
+        flags = []
+        if not any(x.get("issue_type") == "record" for x in kids + linked):
+            flags.append("no record")
+        if kinds.get("task", 0) >= 3 and not kinds.get("decision"):
+            flags.append(f"{kinds['task']} tasks, 0 decisions")
+        if flags:
+            thin.append(f"{root:18} {', '.join(flags):26} 
{beads[root]['title'][:34]}")
+    report("Roots that look thin (a question, not a verdict)", thin)
+
+    scope = f"subtree of {args.root}" if scoped else "whole database"
+    print(f"\n{len(beads)} beads, {len(edges)} edges, {len(roots)} root(s)  
[{scope}]")
+
+
+if __name__ == "__main__":
+    main()

Reply via email to