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()
