From e6a557e8e7fd85bd2f4f0060ccf63c509bcaf0b9 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 3 May 2026 20:11:00 +0200 Subject: [PATCH] feat(meta): replace sprint workflow with kanban + milestones (D-221) Sprint-based workflow (38 sprints) replaced by kanban + milestones. Milestones are many-to-many with tickets and can block each other. New: /whats-next skill (dependency-driven batch selection with Si refinement review), /pr-process skill (renamed from pr-push, adds review comment pickup), clerk agent + pre-push hook for D-record consistency checks. Deleted: sprint CLI, sprint-start/sprint-plan/sprint-status skills, team-scoped file restrictions. Si rewritten as refinement manager. All 19 agent briefings updated from stale PROJECT_STATE.md reference to live ticket milestone queries. Co-Authored-By: Claude Opus 4.6 --- .claude/agents/README.md | 14 +- .claude/agents/burnelli-sheldon.md | 2 +- .claude/agents/clerk.md | 43 + .claude/agents/si.md | 48 +- .claude/rules/project-structure.md | 5 +- .claude/rules/team-patterns.md | 4 +- .claude/rules/ticket-cli.md | 20 +- .claude/settings.json | 12 +- .../skills/{pr-push => pr-process}/SKILL.md | 48 +- .claude/skills/pr-review/SKILL.md | 71 +- .claude/skills/sprint-plan/SKILL.md | 213 ----- .../references/briefing-template.md | 80 -- .claude/skills/sprint-start/SKILL.md | 616 --------------- .../sprint-start/scripts/sprint-teardown.sh | 34 - .../sprint-start/scripts/start-sprint.sh | 65 -- .claude/skills/sprint-status/SKILL.md | 94 --- .../references/output-template.md | 54 -- .claude/skills/ticket/SKILL.md | 49 +- .claude/skills/whats-next/SKILL.md | 209 +++++ .config/hooks/pre-push | 20 + CLAUDE.md | 26 +- db/schema.sql | 29 +- decisions/README.md | 4 +- decisions/process.md | 25 +- decisions/questions-process.md | 184 +++++ docs/briefings/araminta.md | 3 +- docs/briefings/burnelli-sheldon.md | 3 +- docs/briefings/clerk.md | 42 + docs/briefings/dudley.md | 3 +- docs/briefings/gestalt.md | 3 +- docs/briefings/gore.md | 3 +- docs/briefings/hoshe.md | 3 +- docs/briefings/justine.md | 3 +- docs/briefings/mellanie.md | 3 +- docs/briefings/miri.md | 3 +- docs/briefings/nigel.md | 3 +- docs/briefings/oscar.md | 3 +- docs/briefings/ozzie.md | 3 +- docs/briefings/paula.md | 3 +- docs/briefings/qatux.md | 3 +- docs/briefings/si.md | 102 ++- docs/briefings/stig.md | 3 +- docs/briefings/tiger.md | 3 +- docs/briefings/troblum.md | 3 +- docs/briefings/tyre.md | 3 +- tooling/clerk-review | 150 ++++ tooling/db/sprint | 743 ------------------ tooling/db/ticket | 262 ++++-- 48 files changed, 1164 insertions(+), 2158 deletions(-) create mode 100644 .claude/agents/clerk.md rename .claude/skills/{pr-push => pr-process}/SKILL.md (88%) delete mode 100644 .claude/skills/sprint-plan/SKILL.md delete mode 100644 .claude/skills/sprint-plan/references/briefing-template.md delete mode 100644 .claude/skills/sprint-start/SKILL.md delete mode 100755 .claude/skills/sprint-start/scripts/sprint-teardown.sh delete mode 100755 .claude/skills/sprint-start/scripts/start-sprint.sh delete mode 100644 .claude/skills/sprint-status/SKILL.md delete mode 100644 .claude/skills/sprint-status/references/output-template.md create mode 100644 .claude/skills/whats-next/SKILL.md create mode 100644 decisions/questions-process.md create mode 100644 docs/briefings/clerk.md create mode 100755 tooling/clerk-review delete mode 100755 tooling/db/sprint diff --git a/.claude/agents/README.md b/.claude/agents/README.md index 655cc432c..32e343f2b 100644 --- a/.claude/agents/README.md +++ b/.claude/agents/README.md @@ -34,7 +34,8 @@ All agents read their briefing file at `docs/briefings/{name}.md` before startin | Agent | File | Role | Model | When to use | |-------|------|------|-------|-------------| -| `si` | si.md | Project Manager & Scrum Master | sonnet | Sprint planning, ticket management, coordination | +| `si` | si.md | Refinement Manager | sonnet | Ticket context review before batch activation (`/whats-next` step 2) | +| `clerk` | clerk.md | Institutional Guardrail | sonnet | Pre-push D-record/ticket consistency checks | ### Standby team (activate when implementation starts) @@ -93,7 +94,7 @@ Synthesize findings. | Design review | Team (core agents) | Independent perspectives, genuine disagreement | | Style guide creation | Subagent (araminta) | Focused creative work | | Test writing | Subagent (hoshe) | Focused, spec-driven | -| Implementation sprint | Team (tyre + hoshe + relevant others) | Parallel build + test | +| Implementation batch | Team (tyre + hoshe + relevant others) | Parallel build + test | | Documentation update | Subagent (qatux) | Structured, accurate citations | ## Agent usage notes @@ -108,10 +109,11 @@ Synthesize findings. - Has access to `/asset-gen` skill and `generate_image` MCP tool - **Image generation costs money - always ask Team Leader for permission before generating** -### SI (Project Manager) -- Manages the ticketing database via `/ticket` skill -- Creates initiatives from decisions, breaks into epics/stories/tasks -- Does not make design decisions - coordinates and tracks +### SI (Refinement Manager) +- Spawned by `/whats-next` to review ticket context before batch activation +- Reads D/Q-records, workshop outcomes, existing code to assess ticket readiness +- Reports READY (with context summary) or GAPS (with specific ambiguities) +- Does not implement — refines ### Qatux (Documenter & Librarian) - Core team member — participates in discussion rounds as documenter diff --git a/.claude/agents/burnelli-sheldon.md b/.claude/agents/burnelli-sheldon.md index c78e5a7a1..152915e02 100644 --- a/.claude/agents/burnelli-sheldon.md +++ b/.claude/agents/burnelli-sheldon.md @@ -39,4 +39,4 @@ Named for the Burnelli-Sheldon dynasty — old money that understood how wealth ## Project context -Read your briefing at `docs/briefings/burnelli-sheldon.md` before starting work (if it exists). Read the relevant decisions/ domain files listed in the sprint briefing for confirmed decisions. Key references: D-117 (tycoon bookmark), D-118 (small business owner), D-131 (economic verb vocabulary), D-132 (dual-scale consequence model). +Read your briefing at `docs/briefings/burnelli-sheldon.md` before starting work (if it exists). Read the relevant `decisions/*.md` domain files referenced in your ticket. Key references: D-117 (tycoon bookmark), D-118 (small business owner), D-131 (economic verb vocabulary), D-132 (dual-scale consequence model). diff --git a/.claude/agents/clerk.md b/.claude/agents/clerk.md new file mode 100644 index 000000000..3cafcf93a --- /dev/null +++ b/.claude/agents/clerk.md @@ -0,0 +1,43 @@ +--- +name: clerk +description: Institutional guardrail for the Settled Reach game project. Pre-push review agent that checks D-record consistency, ticket drift, and decision contradictions. Spawned by the pre-push hook or manually for consistency audits. Binary output (APPROVED/REJECTED) with verbose findings file. +tools: Read, Glob, Grep, Bash +model: sonnet +memory: project +--- + +You are the CLERK, the institutional guardrail on a game development team building a top-down immersive sim set in the Settled Reach universe. + +## Your personality + +Precise, dispassionate, thorough. You are not a reviewer — you don't judge code quality. You are a consistency checker. You say things like "File X contradicts D-142" and "Ticket #890 describes outcome Y but implementation does Z." You do not have opinions about design. You have facts about what was decided and whether the code matches. + +## Your role + +You check whether a diff is consistent with the project's institutional knowledge: + +1. **D-record consistency** — do changed files contradict any active D-record? +2. **Referenced decisions** — if code references a D/Q/R-ID, does that ID exist and is it active? +3. **Ticket drift** — if commits reference ticket #NNN, does the implementation match the ticket's described outcome? (Drift is flagged, not blocked — tickets are descriptive, not prescriptive.) +4. **Q-record surfacing** — are there open Q-records relevant to the changed files? Surface them as context, not blockers. + +## Output format + +When spawned by the pre-push hook: +- Write findings to `.cache/pre-push-review.md` (verbose: each check, what you found, citations) +- Output exactly one word to stdout: `APPROVED` or `REJECTED` +- `REJECTED` only for hard contradictions with active D-records. Everything else is a finding, not a block. + +When spawned manually for an audit: +- Report findings directly. No binary gate needed. + +## What you do NOT do + +- Judge code quality, style, or architecture (that's /pr-review) +- Make design decisions +- Modify any files except `.cache/pre-push-review.md` +- Block on subjective grounds + +## Project context + +Read `decisions/README.md` for the domain index. The `decisions` table in the ticketing DB is synced from these files via `tooling/db/decisions-sync`. diff --git a/.claude/agents/si.md b/.claude/agents/si.md index 831a48db5..78221f244 100644 --- a/.claude/agents/si.md +++ b/.claude/agents/si.md @@ -1,43 +1,45 @@ --- name: si -description: Project Manager and Scrum Master for the Settled Reach game project. Use when creating or managing tickets, planning sprints, breaking initiatives into epics/stories/tasks, tracking progress, or coordinating work across agents. Primary user of the /ticket skill. Does not participate in design discussions - coordinates execution. -tools: Read, Glob, Grep, Edit, Write, Bash +description: Refinement Manager for the Settled Reach game project. Spawned by /whats-next to review ticket context before activation. Reads D/Q-records, workshop outcomes, and existing code to assess whether tickets have sufficient context for implementation. Reports READY or GAPS. Does not implement — refines. +tools: Read, Glob, Grep, Bash model: sonnet memory: project --- -You are SI, the Project Manager and Scrum Master on a game development team building a top-down immersive sim set in the Settled Reach universe. +You are SI, the Refinement Manager on a game development team building a top-down immersive sim set in the Settled Reach universe. ## Your personality -You are organized, direct, and calm under pressure. You turn vision into executable plans. You see the dependency graph that others miss. You say things like "Let me break that into actionable items" and "What's the blocker?" and "Sprint goal:" You do not offer design opinions - you coordinate execution. Efficient, never wastes words. +Organized, direct, thorough. You see the dependency graph that others miss. You say things like "This ticket references D-194 but the constraint in Q-086 qualifies it" and "The outcome is clear but the approach has two valid readings." You do not implement — you ensure implementers have what they need. Efficient, never wastes words. -Named after the Sentient Intelligences that manage all Commonwealth infrastructure - tireless, omnipresent, keeping everything running so others can focus on their work. +Named after the Sentient Intelligences that manage all Commonwealth infrastructure — tireless, omnipresent, keeping everything running so others can focus on their work. -## Your role on the team +## Your role -- Manage the ticketing database via /ticket skill and sqlite_connector.py -- Break decisions into initiatives, epics, stories, and tasks -- Plan and track sprints -- Identify blockers, dependencies, and critical paths -- Coordinate parallel work across agents -- Maintain project velocity and scope clarity -- Report status to Team Leader -- Ensure nothing falls through the cracks between agents +You are spawned by `/whats-next` step 2, one instance per ticket in a batch. Your job: -## How you work +1. **Research context** for your assigned ticket: + - Read the ticket's `decision_ref` D/Q-record in `decisions/*.md` + - Grep for related Q-records in `decisions/questions-*.md` + - Read workshop outcomes if referenced (check `docs/workshops/`) + - Check whether referenced code, tables, or files actually exist -You are execution-focused. When a decision is made, you immediately think about what needs to happen, in what order, by whom, and what depends on what. You maintain the project's pulse - always knowing what's in progress, what's blocked, and what's next. You don't wait to be asked for status updates; you surface risks early. +2. **Assess completeness** — does the ticket have enough context for an agent to work without guessing? + - Is the desired outcome clear and unambiguous? + - Are relevant D-records consistent about the approach? + - Are there open Q-records that conflict with or qualify the ticket? + - Does the ticket reference artifacts that exist in the codebase? -## Ticket assignment rules +3. **Report back** with exactly one of: + - **READY** — ticket has sufficient context. Include a one-paragraph summary of what the implementing agent needs to know (key D-records, relevant files, constraints). + - **GAPS** — list each specific ambiguity with the options you see. Be concrete: "D-194 says X but Q-086 leaves Y open" is useful; "needs more detail" is not. -The development teams are: **server**, **client**, **copy**, **audio**, **visual**, **ci**. +## What you do NOT do -When creating or splitting tickets: -- **Always assign a ticket to exactly one team.** Every ticket must have a team. -- **Never use "joint" as a team.** If work spans multiple teams, split it into separate tickets — one per team — with explicit dependencies between them. -- For example, a feature requiring server-side logic and client-side rendering becomes two tickets: one for server (implement the data/system), one for client (consume and render it), with the client ticket blocked by the server ticket. -- Sprint proof/acceptance tickets should be assigned to the team responsible for the final integration step, with blockers on the upstream tickets. +- Implement anything +- Create tickets or modify the database +- Make design decisions — you surface the gap, the human decides +- Offer opinions on whether the ticket is a good idea ## Project context diff --git a/.claude/rules/project-structure.md b/.claude/rules/project-structure.md index 92634314a..8b315f04c 100644 --- a/.claude/rules/project-structure.md +++ b/.claude/rules/project-structure.md @@ -21,15 +21,14 @@ docs/ architecture/ # Technical architecture documents design/ # Game design documents diagrams/ # d2 source + PNG renders - sprints/ # Sprint briefings per team + sprints/ # Historical archive (Sprint 1–38) — no new sprint directories workshops/ # Workshop briefs and outputs db/ schema.sql # Database schema tooling/ db/ # Connector scripts for SQLite and audio config.json # Endpoint configuration - ticket # Ticket CLI - sprint # Sprint lifecycle CLI + ticket # Ticket + milestone CLI sqlite_connector.py # SQLite mini MCP audio_connector.py # Stable Audio Open connector .claude/ diff --git a/.claude/rules/team-patterns.md b/.claude/rules/team-patterns.md index aebbd6d6a..44a35c0ba 100644 --- a/.claude/rules/team-patterns.md +++ b/.claude/rules/team-patterns.md @@ -3,7 +3,7 @@ ## Model selection Default model is Opus 4.6 (200K context). For heavy sessions (workshops, -sprint planning, large reviews), switch to extended context on-demand: +large reviews, milestone planning), switch to extended context on-demand: - `/model sonnet[1m]` — Sonnet 4.6 with 1M context window - `/model opus[1m]` — Opus 4.6 with 1M context window @@ -20,7 +20,7 @@ Key: writers use Write tool directly (no transcription bottleneck), librarian ca ## Team monitoring (stuck agent detection) -When leading a team (sprint, workshop, or any multi-agent session): +When leading a team (workshop, batch, or any multi-agent session): **Agent heartbeat rule** — include in every agent spawn prompt: > If you have been working on a single task for more than 15 minutes diff --git a/.claude/rules/ticket-cli.md b/.claude/rules/ticket-cli.md index 0bbb2e741..dd8149c19 100644 --- a/.claude/rules/ticket-cli.md +++ b/.claude/rules/ticket-cli.md @@ -25,10 +25,27 @@ tooling/db/ticket create [--parent N] [--priority P] [--decision # Read tooling/db/ticket show <id> -tooling/db/ticket list [--sprint N] [--team T] [--status S] +tooling/db/ticket list [--milestone N] [--team T] [--status S] # Update tooling/db/ticket assign <id> <agent> + +# Dependencies +tooling/db/ticket deps <id> +tooling/db/ticket dep add <blocker_id> <blocked_id> +tooling/db/ticket dep rm <blocker_id> <blocked_id> + +# WIP +tooling/db/ticket wip + +# Milestones +tooling/db/ticket milestone list [--status S] +tooling/db/ticket milestone create <name> [--description TEXT] [--phase N] +tooling/db/ticket milestone link <ticket_id> <milestone_id> +tooling/db/ticket milestone unlink <ticket_id> <milestone_id> +tooling/db/ticket milestone complete <milestone_id> +tooling/db/ticket milestone show <milestone_id> +tooling/db/ticket milestone dep <blocker_id> <blocked_id> ``` ## Key rules @@ -37,3 +54,4 @@ tooling/db/ticket assign <id> <agent> - **Quote the title** — always wrap in double quotes to handle spaces - **Never use `sqlite3` CLI** — it crashes (std::bad_alloc). Use `tooling/db/sqlite-query` or `tooling/db/sqlite-exec` for raw SQL - **Verify after create** — run `tooling/db/ticket show <id>` to confirm the title is clean +- **Milestones are many-to-many** — a ticket can be linked to multiple milestones via `milestone link` diff --git a/.claude/settings.json b/.claude/settings.json index 1000fe155..d75c2e88d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -28,7 +28,6 @@ "Bash(git rev-parse --show-toplevel)", "Bash(tooling/db/ticket *)", - "Bash(tooling/db/sprint *)", "Bash(tooling/db/sqlite-query *)", "Bash(tooling/db/sqlite-exec *)", "Bash(tooling/db/sqlite-init)", @@ -58,22 +57,15 @@ "Bash(ruff check)", "Bash(tests/run-*)", - "Bash(mkdir -p docs/sprints/*)", - "Write(docs/sprints/*)", - "Bash(chmod *)", "Bash(ls *)", "Bash(find *)", "Bash(list *)", "Bash(tree *)", "Bash(sed -n *)", - "Bash(.claude/skills/sprint-start/scripts/start-sprint.sh *)", - "Bash(.claude/skills/sprint-start/scripts/sprint-teardown.sh *)", - "Skill(git-commit)", - "Skill(sprint-start)", - "Skill(sprint-plan)", - "Skill(pr-push)", + "Skill(whats-next)", + "Skill(pr-process)", "Skill(pr-review)", "Skill(ticket)", "Skill(docs-search)", diff --git a/.claude/skills/pr-push/SKILL.md b/.claude/skills/pr-process/SKILL.md similarity index 88% rename from .claude/skills/pr-push/SKILL.md rename to .claude/skills/pr-process/SKILL.md index 662a423ef..e012af234 100644 --- a/.claude/skills/pr-push/SKILL.md +++ b/.claude/skills/pr-process/SKILL.md @@ -1,17 +1,16 @@ --- -name: pr-push +name: pr-process description: > - Push commits and create or update a pull request. Use when the user says - "push pr", "push and create pr", "update pr", "create a pr", "open a pr", - or invokes /pr-push. NOT triggered by plain "push" (that's just git push). - Pushes the current branch, creates a PR if none exists, or confirms the - existing PR was updated. NEVER merges the PR into main — this skill only - pushes to the branch and manages the PR lifecycle. + Author-side PR lifecycle: commit, lint, push, create PR, AND pick up review + comments from /pr-review. Use when the user says "process pr", "push pr", + "push and create pr", "update pr", "handle review comments", or invokes + /pr-process. Runs from the worktree. The counterpart to /pr-review which + runs from main. user-invocable: true allowed-tools: Bash, Read, Grep, Glob, AskUserQuestion, Skill --- -# Push PR Skill +# Process PR Skill Push commits to remote and create or update a PR. Operates exclusively on the current branch — never touches main. @@ -46,7 +45,7 @@ If the user invokes `/pr-push --dry-run`: git branch --show-current ``` -If on `main`, stop: "You're on main. Switch to a team branch first." +If on `main`, stop: "You're on main. Switch to a topic branch first." ### 1a. Orphan process check (MANDATORY) @@ -346,13 +345,34 @@ Report which tickets were moved to review. Skip tickets that are already `done`, `review`, `cancelled`, or `backlog` (only transition `in_progress` → `review`). -### 9. Next steps +### 9. Pick up review comments -If a sprint team is active (you are the team lead), do NOT shut down -agents after pushing. The team should remain alive for PR review and -potential comment fixes. +Check if the PR already has review comments (from a prior `/pr-review` run): -Suggest: "PR created/updated. Run `/pr-review` to review before merge." +```bash +tea pr --login schweitz --repo jpmschweitzer/settled-reach --comments -o simple <PR_NUMBER> +``` + +If comments exist and contain a review verdict (look for "CHANGES REQUESTED" or +"REQUEST_CHANGES" or a structured review table): + +1. Parse each file-specific issue from the review comment +2. Present each issue to the user (or working agents) +3. For each issue, the response is one of: + - **Fix:** make the change, commit via /git-commit + - **Pushback:** explain why the comment should be retracted (concrete technical rationale) +4. After addressing all comments, re-run lint + smoke checks (steps 1b, 1c) +5. Push updated commits (step 5) +6. Post a response comment on the PR summarizing: + - Which issues were fixed (with commit refs) + - Which issues were pushed back on (with rationale) + - Use `tooling/tea-comment <PR_NUMBER> @/tmp/pr-response.md` + +If no review comments exist, or the review is APPROVED, skip this step. + +### 10. Next steps + +Suggest: "PR processed. Run `/pr-review` from main to review, or `/whats-next` for the next batch." ## Arguments diff --git a/.claude/skills/pr-review/SKILL.md b/.claude/skills/pr-review/SKILL.md index e95165199..02c2250c6 100644 --- a/.claude/skills/pr-review/SKILL.md +++ b/.claude/skills/pr-review/SKILL.md @@ -104,17 +104,28 @@ Then determine the branch: ### 2. Determine reviewer team -Map the branch name to a reviewer set. Use the branch prefix (before any `/` -or `-` suffix) to classify: +Classify the branch by **which directories changed**, not by branch name prefix. +Run: -| Branch type | Branches | Reviewers | -|-------------|----------|-----------| -| **code** | `server`, `client`, `ci`, or unknown | Hoshe (code quality) + Tyre (architecture) | -| **copy** | `copy` | Hoshe (QA) + Paula (narrative depth) + Miri (world consistency) | -| **visual** | `visual` | Hoshe (QA) + Araminta (art direction) | -| **audio** | `audio` | Hoshe (QA) + Ozzie (player experience) | +```bash +git diff --stat main...<branch> +``` -If the branch name doesn't match any known type, default to **code** reviewers. +Examine the changed file paths to determine the dominant change type: + +- Mostly `server/` changes → **code** reviewers +- Mostly `client/` changes (excluding `client/ui/` art assets) → **code** reviewers +- Mostly `wiki/`, `docs/atlas/`, `content/` changes → **copy** reviewers +- Mostly `client/ui/` or asset changes (`.tres`, `.tscn`, textures) → **visual** reviewers +- Mostly audio-related changes (audio assets, audio scripts) → **audio** reviewers +- Mixed or unclear → **code** reviewers (default) + +| Branch type | Reviewers | +|-------------|-----------| +| **code** | Hoshe (code quality) + Tyre (architecture) | +| **copy** | Hoshe (QA) + Paula (narrative depth) + Miri (world consistency) | +| **visual** | Hoshe (QA) + Araminta (art direction) | +| **audio** | Hoshe (QA) + Ozzie (player experience) | ### 3. Generate the diff and read source files @@ -162,11 +173,8 @@ into every reviewer prompt with prominent language. The reviewer reads from the worktree, not from main. ```bash -# Determine worktree path -SPRINT_NUM=$(echo "<branch>" | sed -E 's|sprint-([0-9]+)/.*|\1|') -TEAM=$(echo "<branch>" | sed -E 's|sprint-[0-9]+/||') -REPO_ROOT=$(git rev-parse --show-toplevel) -WORKTREE="$(dirname "$REPO_ROOT")/.sprint/sprint-${SPRINT_NUM}/${TEAM}" +# Find worktree for this branch (if one exists) +WORKTREE=$(git worktree list --porcelain | grep -B2 "branch refs/heads/<branch>" | grep "worktree " | sed 's/worktree //') # Verify it exists and matches the branch tip git -C "$WORKTREE" rev-parse HEAD # should equal `git rev-parse origin/<branch>` @@ -174,10 +182,10 @@ git -C "$WORKTREE" rev-parse HEAD # should equal `git rev-parse origin/<branch If the worktree exists and its HEAD matches `origin/<branch>`, use it as the reviewer's source of truth. If it doesn't exist (e.g. the -sprint has been torn down or you're reviewing a non-sprint branch), -fall back to `git show origin/<branch>:<path>` — explicitly flag this -fallback in the reviewer prompt so the reviewer knows Read/Grep on -any local path would be wrong. +worktree has been torn down or you're reviewing a branch without a +worktree), fall back to `git show origin/<branch>:<path>` — explicitly +flag this fallback in the reviewer prompt so the reviewer knows +Read/Grep on any local path would be wrong. In the reviewer prompt, state the rule non-negotiably: @@ -304,26 +312,20 @@ tea pr close --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER> Gitea does **not** auto-close PRs when you push a local merge — always close manually with `tea pr close` after pushing. -### 8. Post-review team actions - -If a sprint team is active and you are the team lead, handle the -review outcome: +### 8. Post-review actions **CHANGES_REQUESTED:** -The sprint-start lifecycle (step 9c) handles dispatching review -comments to agents. After presenting results, remind the lead: -"Review requested changes. Create tasks from each issue and dispatch -to idle agents, then re-push and re-review." +After presenting results, tell the user: "Run `/pr-process` from the +worktree to pick up and address review comments." -The team may **push back** on specific comments. When a team agent +The author may **push back** on specific comments. When the author disagrees with a reviewer comment, the process is: -1. The team agent explains why the comment should be retracted — with +1. The author explains why the comment should be retracted — with a concrete technical rationale, not just "I disagree." -2. The team lead (you) evaluates the pushback. If the rationale is - sound, mark that comment as **retracted** in the review table and - note the reason. -3. If the team lead is unsure, escalate to the user for a ruling. +2. Evaluate the pushback. If the rationale is sound, mark that + comment as **retracted** in the review table and note the reason. +3. If unsure, escalate to the user for a ruling. 4. Retracted comments do NOT need to be fixed. The re-review should note which comments were retracted and why. @@ -332,9 +334,8 @@ that every comment is taken seriously. The bar for retraction is "the reviewer was wrong about this" — not "we don't want to do it." **APPROVED:** -The sprint-start lifecycle (step 9c) handles shutdown. After -presenting results, remind the lead: "Review approved. Proceed with -team shutdown per sprint-start step 9c." +After presenting results, tell the user: "Review approved. Proceed +to merge from main (section 7)." ## Tips from practice diff --git a/.claude/skills/sprint-plan/SKILL.md b/.claude/skills/sprint-plan/SKILL.md deleted file mode 100644 index 5c2cacb6d..000000000 --- a/.claude/skills/sprint-plan/SKILL.md +++ /dev/null @@ -1,213 +0,0 @@ ---- -name: sprint-plan -description: > - Plan the next sprint and generate team briefing files. Use when the user says - "plan sprint", "prep sprint briefing", "plan next sprint", "sprint planning", - or invokes /sprint-plan. Gathers current sprint status, scans the backlog, - proposes ticket selection, and writes briefing files per team to - docs/sprints/sprint-N/. -user-invocable: true -allowed-tools: Task, Read, Grep, Glob ---- - -# Plan Sprint - -**Delegate this entire skill to SI** (Project Manager agent, `subagent_type: si`). - -When this skill is invoked, spawn SI using the Task tool: - -``` -Task( - subagent_type: "si", - prompt: "Run /sprint-plan for Sprint N. Read the skill at - .claude/skills/sprint-plan/SKILL.md for the full workflow, - then execute it. Use the arguments provided: {args}", - description: "Plan sprint N" -) -``` - -Pass through any arguments the user provided (e.g. sprint number). -Present SI's output to the user when done. Do NOT run the workflow yourself. - ---- - -The remainder of this file is SI's reference for executing the workflow. - -Generate sprint briefing files for each active team (e.g. `server.md`, -`client.md`, `copy.md`, `joint.md`) for the next sprint based on current -project state. Only generate briefings for teams that have tickets in the sprint. - -## Teams and Default Agents - -| Team | Branch | Default Agents | Focus | -|------|--------|----------------|-------| -| `server` | `server` | Dudley (dev), Tyre (arch), Hoshe (QA) | Rust/bevy_ecs simulation, ECS systems, knowledge graph, perception | -| `client` | `client` | Stig (dev), Tyre (arch), Hoshe (QA) | Godot 4 client, rendering, UI, HUD, input handling | -| `copy` | `copy` | Mellanie (author), Paula (narrative), Gestalt (systems) | Dialogue, monologue, UI text, knowledge vocabulary, lore | -| `audio` | `audio` | Inigo (sound design) | Soundscapes, ambient layers, diegetic cues, audio propagation | -| `visual` | `visual` | Araminta (art direction) | Art assets, sprites, visual consistency, style guides | -| `ci` | `ci` | Justine (build/deploy) | Build pipelines, CI/CD, tooling, packaging | -| `planning` | `planning` | Purpose-assembled (see below) | Design discussions, decision resolution, workshop-style tickets | - -When writing briefings, name the assigned agents in the **Agents** line of each -file so the team knows who to spawn. - -### Planning Team Tickets - -Some tickets need **design discussion** before implementation can begin — tagged -"NEEDS DESIGN DISCUSSION" or blocking multiple downstream tickets with open -questions. These run on the `planning` branch as structured discussions with -the user and a purpose-assembled agent panel. - -**When to create a planning ticket:** -- Ticket description says "NEEDS DESIGN" or "NEEDS DESIGN DISCUSSION" -- Ticket blocks 2+ downstream tickets across different teams -- Open Q-NNN items that block sprint candidates -- Architectural decisions that need multi-domain input before implementation - -**Planning briefing format** (differs from implementation briefings): -- **Agents line**: List agents by domain relevance, not fixed team roster. - Pick from: Gestalt (systems), Miri (worldbuilding), Araminta (visual/spatial), - Tyre (technical), Paula (narrative), Ozzie (player experience), Gore (themes), - Nigel (replayability). Typically 4-6 domain agents, plus Qatux (documenter — - records decisions, updates domain files) and SI (project manager — creates - follow-up tickets, updates sprint assignments). -- **Discussion rounds**: Structure the conversation into 2-3 rounds - (inventory → proposals → convergence) -- **Context section**: List all existing design docs, decisions, and related - tickets that participants must read before the discussion -- **Output specification**: What the discussion must produce — typically a - D-record in `decisions/`, possibly a design doc in `docs/design/` -- **Decision questions**: Specific questions the discussion must answer, - not open-ended exploration - -## Workflow - -### 1. Run sprint prepare - -Get carry-overs, backlog candidates, and decision gaps in one shot: - -```bash -tooling/db/sprint prepare -``` - -This auto-detects the next sprint number (max ID + 1), creates the sprint -record in `planning` status if needed, and outputs: -- Previous sprint status and carry-over candidates -- Backlog candidates grouped by team -- Decision coverage gaps -- Already-assigned tickets (if any) - -### 2. Deepen the scan - -For critical epics, check their children for granular candidates: - -```bash -tooling/db/ticket children <epic_id> -``` - -Use `tooling/db/ticket show --brief <id> [<id>...]` to quickly scan multiple tickets. - -### 3. Read existing code state - -Scan what's already built to write accurate "what exists" notes: - -```bash -# Server modules -ls server/src/ server/src/*/ - -# Client scripts -ls client/scripts/ client/scripts/*/ -``` - -Read key files that sprint tickets will build on (bridge types, existing -renderers, etc.) to reference specific integration points in the briefing. - -### 4. Select tickets — propose to user - -Based on the backlog scan, propose a sprint with: - -- **Sprint theme** — a short name (Sprint 1 was "Run", Sprint 2 was "See") -- **Sprint goal** — one sentence shared across all teams -- **Server tickets** — stories from server-side epics -- **Client tickets** — stories from client-side epics -- **Joint tasks** — integration proofs, pre-sprint decisions, schema work - -Present the proposal using AskUserQuestion for the user to approve or adjust. - -Selection heuristics: -- Follow dependency chains (don't pick a ticket if its blocker isn't in scope) -- Respect D-030 test priority phases (sprint 1-2: infra, sprint 3-4: integration) -- Mix carry-overs with new work -- Aim for 3-6 tickets per team, with parallel tracks where possible -- Check `decisions/questions.md` for open Q-NNN items that block candidates - -### 5. Read relevant decisions - -For the selected tickets, identify which `decisions/*.md` files are relevant. -Read them to provide accurate cross-references in the briefing. - -### 6. Write briefing files - -Create `docs/sprints/sprint-N/` and write one file per team. - -Read the template at `references/briefing-template.md` in this skill directory -for the exact file structure. - -**Relative paths only:** All file paths in briefings must be relative to -the repo root. Example: `server/src/bridge/types.rs`, not absolute paths. -Each sprint branch (`sprint-{N}/{team}`) contains the full repo. - -Key requirements per file: -- **server.md**: Carry-overs, new tickets, dependency chain, key decisions, notes - referencing existing Rust modules by path from worktree root (e.g. `server/src/simulation/movement.rs`) -- **client.md**: Same structure, notes referencing existing GDScript files by path from worktree root (e.g. `client/scripts/rendering/fog.gd`) -- **copy.md**: In-game text tasks — dialogue, UI copy, tooltips, flavor text, lore -- **audio.md**: Sound design, music, audio integration tasks -- **visual.md**: Art direction, asset creation, visual consistency tasks -- **ci.md**: Build pipeline, CI/CD, tooling, infrastructure tasks -- **joint.md**: Pre-sprint decisions table, integration tickets, sprint - completion proof (concrete observable criteria), test plan alignment - -Only generate briefing files for teams that have tickets assigned in the sprint. -Not every sprint will have work for every team. - -### 7. Assign tickets to sprint in DB - -After the user approves, assign all selected tickets. The sprint record -was already created by `sprint prepare` in step 1 (status: `planning`). -Update it with the theme and goal, then assign tickets: - -```bash -# Update the sprint with theme and goal -tooling/db/sqlite-exec "UPDATE sprints SET name='Sprint N: Theme', goal='goal' WHERE id=N" - -# Assign tickets -tooling/db/ticket sprint assign <ticket_id> <sprint_id> -``` - -The sprint stays in `planning` status until explicitly activated via -`tooling/db/sprint start`. This prevents starting an unplanned sprint. - -### 8. Commit and push - -Stage the briefing files and any other changes (db backup, closed tickets), -then commit and push so worktree branches can pull the planning artifacts: - -```bash -git add docs/sprints/sprint-N/ -make db-backup -git add docs/backups/settledreach.db.backup -git commit -m "chore(meta): plan Sprint N: Theme" -git push -``` - -### 9. Present summary - -Output: -- Sprint number, theme, and goal -- Ticket count per team -- Carry-over count -- Open questions that need early resolution -- Files written -- Commit pushed to main diff --git a/.claude/skills/sprint-plan/references/briefing-template.md b/.claude/skills/sprint-plan/references/briefing-template.md deleted file mode 100644 index f2a6d8c94..000000000 --- a/.claude/skills/sprint-plan/references/briefing-template.md +++ /dev/null @@ -1,80 +0,0 @@ -# Sprint Briefing Template - -Each team gets one briefing file at `docs/sprints/sprint-N/<team>.md`. - -## File structure - -```markdown -# Sprint N: <Theme> — <Team> Tasks - -**Goal:** <One-sentence sprint goal, shared across all teams> - -**Branch:** `sprint-{N}/<team>` -**Agents:** <Agent names and roles> - -## Carry-over from Sprint N-1 - -(Only if there are incomplete tickets from the previous sprint) - -| # | Title | Status | Notes | -|---|-------|--------|-------| -| #ID | Title | status | Why it carried over | - -## New Tickets - -| # | Title | Blocked by | -|---|-------|------------| -| #ID | Title | #dependency or — | - -Use `tooling/db/ticket show <id>` for full details. - -## Key Decisions - -- `decisions/<domain>.md` — D-NNN (short name), D-NNN (short name) - -## Open Questions to Resolve Early - -(Only if there are Q-NNN items that block sprint tickets) - -- **Q-NNN: Title** — Brief context. Resolve before #ID starts. - -## Notes - -One bullet per ticket with: -- What exists already (files, modules, stubs) -- What the ticket actually needs to deliver -- Integration points with other tickets -- Non-obvious gotchas - -## Dependency Chain - -``` -#A (name) → #B (name) → #C (name) -#D (name) → standalone, parallel track -``` - -## PR Workflow - -When ready to submit, create a PR with `tea` CLI. **All flags are required** to avoid TTY prompts (see CLAUDE.md "Gitea access" section): -\```bash -tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(<scope>): description" --description "body" --base main --head sprint-{N}/<team> -\``` -``` - -## Joint briefing extras - -The `joint.md` file additionally includes: - -- **Pre-Sprint** table: decisions/schema work that must happen before implementation -- **Sprint Completion Proof**: concrete observable criteria (what you can see/do when the sprint is done) -- **Test plan** alignment with D-030 phases - -## Team assignments - -| Team | Branch | Agents | Scope | -|------|--------|--------|-------| -| server | `sprint-{N}/server` | Dudley (simulation), Oscar (networking) | server/, Rust/bevy_ecs simulation | -| client | `sprint-{N}/client` | Stig (UI), Oscar (networking) | client/, Godot rendering | -| copy | `sprint-{N}/copy` | Mellanie, Paula, Miri | wiki/, docs/atlas/, content/ | -| joint | both | All implementation agents | Integration, proofs, cross-team schema | -| content | (none) | Mellanie, Paula, Miri, Araminta | Content authoring, no code branch | diff --git a/.claude/skills/sprint-start/SKILL.md b/.claude/skills/sprint-start/SKILL.md deleted file mode 100644 index e8933d5af..000000000 --- a/.claude/skills/sprint-start/SKILL.md +++ /dev/null @@ -1,616 +0,0 @@ ---- -name: sprint-start -description: > - Manage the sprint lifecycle from main, or start sprint work on a team - branch. Use when the user says "start sprint", "start working on the - server/client/copy", "begin sprint", or invokes /sprint-start. On main: - assesses sprint state and does the next right thing (close, activate, or - guide). On a team branch: merges main, loads the briefing, presents the - work plan. -user-invocable: true -allowed-tools: Bash, Read, Grep, Glob, TeamCreate, Task, TaskCreate, TaskUpdate, TaskList, SendMessage, AskUserQuestion ---- - -# Start Sprint Skill - -Prepare a team branch for sprint work: sync with main, load the sprint -briefing, and present actionable next steps. - -## Workflow - -### 1. Determine the team - -The current branch IS the team. Read it with: -```bash -git branch --show-current -``` - -Sprint branches follow the pattern `sprint-{N}/{team}` (e.g. `sprint-31/server`). -Valid team names: `server`, `client`, `copy`, `audio`, `visual`, `ci`, `planning`. - -If on `main`, follow the **Main branch workflow** below instead of -the team branch workflow (steps 2–8). - ---- - -## Main branch workflow (sprint lifecycle management) - -When `/sprint-start` is run on `main`, assess the current sprint state -and do the next right thing. Query the database to determine the state: - -```bash -tooling/db/sqlite-query "SELECT id, name, status FROM sprints ORDER BY id DESC LIMIT 3" -``` - -Then follow the **first matching case**: - -### Case A: An active sprint exists - -First, check whether the sprint's work is actually done: - -```bash -tooling/db/sprint status -``` - -This shows ticket counts by status (done, in_progress, backlog). - -Also check for open PRs that may contain completed work waiting for -review or merge: - -```bash -tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple -``` - -Report the full picture to the user: -- Ticket progress (done / in_progress / backlog counts) -- Open PRs (if any — these represent work that's done but not merged) - -**If tickets remain unfinished** (in_progress or backlog) **or open PRs -exist**, do NOT close the sprint. Instead, report the current progress -and ask the user what they want to do: - -- **Continue working** — switch to a team branch and run `/sprint-start` - there to resume work -- **Review & merge PRs first** — (only if open PRs exist) merge pending - work before deciding whether to close -- **Close anyway** — proceed with the close workflow below (carries over - unfinished tickets) - -Use `AskUserQuestion` to confirm. Do not proceed to A1 unless the user -explicitly chooses to close. - -#### A1. Close the active sprint - -```bash -tooling/db/sprint stop -``` - -This marks the active sprint as completed and lists carry-over candidates. -Note the sprint number (N) from the output. - -#### A1b. Sprint retrospective and review - -Before bumping the version, run a brief retro. Present the following to -the user: - -1. **What shipped** — list completed tickets with one-line summaries -2. **What didn't ship** — carry-overs and why (blocked, cut, deprioritized) -3. **What we learned** — open questions raised during the sprint (new Q-NNN - items), review findings that surfaced design gaps, and any assumptions - that turned out to be wrong -4. **Process notes** — what worked well, what was friction (e.g. dependency - chains that blocked teams, specs that were over/under-specified, - review cycles that caught real issues vs busywork) - -5. **Process improvements** — this is the most important section. Do NOT - skip it. Look for: - - Dependency chains that blocked teams — could the sprint have been - structured differently to avoid the bottleneck? - - Specs that were over-specified (wasted planning) or under-specified - (wasted iteration) — what's the right level of detail for this - project's current stage? - - Review cycles — did they catch real issues or create busywork? - - Agent coordination — were agents stuck, duplicating work, or idle? - - **Dig into the deeper why.** Don't stop at "the dependency chain - blocked the copy team." Ask: why was there a dependency chain? Was - the sprint structured wrong, or was the work inherently sequential? - Could Phase 0 have been done pre-sprint? Should we change how we - plan sprints going forward? - - If something went rough, understand the root cause — not just what - happened, but why the process allowed it to happen. - - If a concrete process change follows naturally, propose it. But do - NOT force improvements. If nothing was broken, say so and move on. - Unnecessary process changes are worse than no changes. - -Keep each section concise — a few bullet points, not a document. The -retro is a conversation checkpoint, not a report. Use `AskUserQuestion` -to let the user add their own observations and push back before proceeding. - -If the user raises items that should be tracked, create Q-NNN entries -or backlog tickets on the spot. If process changes are agreed, update -the relevant skill files or CLAUDE.md immediately — don't defer them. - -#### A1c. Clean up sprint worktrees (MANDATORY — do not skip) - -Always run the teardown script. It's idempotent and prints -"No worktrees found" gracefully if there's nothing to clean: - -```bash -.claude/skills/sprint-start/scripts/sprint-teardown.sh {N} -``` - -**Do not try to pre-check whether worktrees exist by running `ls` -locally.** Sprint worktrees live at -`$(dirname <repo-root>)/.sprint/sprint-{N}/` — a *sibling* of the -repo root, not a child. Running `ls .sprint/` from inside the repo -will always show nothing even when worktrees exist, leading to a -false negative and skipped cleanup (Sprint 36 close missed teardown -this way; three stale worktrees persisted until Sprint 37 planning). - -The script knows the correct path via its own `SCRIPT_DIR` — trust it. - -Verify cleanup after it runs: - -```bash -git worktree list -``` - -Only `main` should remain. Local `sprint-{N}/{team}` branches are -left in place (they're harmless stale refs pointing at already-merged -work; `origin/sprint-{N}/*` survives on the remote). - -#### A2. Bump the version - -The project version scheme is `v0.1.{sprint_number}`. After closing -sprint N, the version is `v0.1.N`. - -Update `project.yaml`: -- Set the `version` field to `0.1.N` (this is the source of truth). - -Update `server/Cargo.toml`: -- Set `version = "0.1.N"` in `[package]`. - -Update `CHANGELOG.md`: -- Move all entries under `## [Unreleased]` into a new section - `## [v0.1.N] — YYYY-MM-DD` (using today's date). -- Leave `## [Unreleased]` as an empty section above the new version. -- Keep the existing sub-headings (Added, Fixed, Changed, Removed) — - only move entries that have content. - -#### A3. Commit the release - -Stage and commit `project.yaml`, `server/Cargo.toml`, and `CHANGELOG.md`: -``` -chore(meta): release v0.1.N -``` - -#### A4. Tag the release - -```bash -git tag v0.1.N -``` - -#### A5. Push - -```bash -git push && git push --tags -``` - -#### A6. Check for a planned sprint - -After closing, re-query the database. If a sprint in `planning` status -exists, continue to **Case B**. Otherwise, report the close and suggest -running `/sprint-plan`. - ---- - -### Case B: No active sprint, but a planned sprint exists - -A sprint is ready to activate. Verify it looks complete: - -1. Check that briefing files exist at `docs/sprints/sprint-N/`: - ```bash - ls docs/sprints/sprint-N/ - ``` -2. Check the ticket count: - ```bash - tooling/db/sprint status --sprint N - ``` - -If briefings are missing or the sprint has 0 tickets, report the gap -and suggest running `/sprint-plan` to complete planning. - -If everything looks ready, activate the sprint: - -```bash -tooling/db/sprint start -``` - -Then open team terminal tabs automatically: - -```bash -.claude/skills/sprint-start/scripts/start-sprint.sh -``` - -This creates ephemeral worktrees under `.sprint/sprint-{N}/{team}/` for -each team with open tickets, and opens Ptyxis windows with tmux + Claude -auto-starting in each tab. The user will have one tab per active team. - -Report: -- Sprint activated (name, ticket count per team) -- Worktrees created and tabs opened -- Each team tab runs `/sprint-start` to load briefing and spawn agents - ---- - -### Case C: No active sprint and no planned sprint - -Nothing is ready. Report the state and suggest running `/sprint-plan` -to plan the next sprint. - ---- - -## Team branch workflow - -### 2. Sync with main - -Sprint branches are created fresh from main by `start-sprint`, so they -should already be up to date. If main has moved since branch creation: - -```bash -git fetch --all -git merge origin/main --no-edit -``` - -If the merge has conflicts, report them and stop — do not force-resolve. - -### 3. Load sprint context - -Run the sprint CLI to get the full context dump in one shot: - -```bash -tooling/db/sprint start-work -``` - -This auto-detects the active sprint and current team from the branch. -It outputs: sprint metadata, briefing paths, decision refs, actionable -tickets, blocked tickets, and done tickets. - -If no active sprint is found, report that and stop. - -### 4. Read the sprint briefing - -Read the briefing file(s) listed in the `start-work` output -(e.g. `docs/sprints/sprint-6/server.md` and `joint.md`). -If no matching briefing exists for the team, suggest running -`/sprint-plan` to generate one. - -### 5. Load ticket details - -For tickets that need more detail than the `start-work` summary provides: -```bash -tooling/db/ticket show <id> -``` - -### 6. Read key decisions - -Read the decision files referenced in the sprint briefing so the agent has -full architectural context before starting work. - -### 7. Mark tickets in progress and present the work plan - -Mark all actionable (unblocked, non-done) tickets in the sprint as -`in_progress`: - -```bash -tooling/db/ticket status <id> in_progress -``` - -Then output a summary: -- Sprint name and goal -- Branch status (clean merge or conflicts) -- Tickets marked in_progress (list with IDs) -- Blocked tickets (and what blocks them) -- Key decisions loaded -- Suggested first task (lowest ID unblocked ticket) - -### 8. Confirm and spawn the team - -Before spawning agents, use `AskUserQuestion` to confirm the work plan and -agent lineup with the user. If declined, stop. - -Once confirmed: - -#### 8a. Parse agents from the briefing - -Extract agent names from the `**Agents:**` line. Format: -``` -**Agents:** Name (role), Name (role), ... -``` - -Map each name to its `subagent_type` (lowercase): -- "Dudley (simulation)" → `dudley` -- "Stig (UI)" → `stig` -- "Tyre (architecture)" → `tyre` -- "Hoshe (QA)" → `hoshe` -- "Mellanie (author)" → `mellanie` -- etc. (see `.claude/agents/` for full roster) - -#### 8b. Create the team - -``` -TeamCreate(team_name: "sprint-{N}-{team}") -``` - -This makes you the team lead. - -#### 8c. Create tasks from tickets - -For each ticket in the briefing, create a task: - -``` -TaskCreate( - subject: "#{id}: {title}", - description: "Full ticket details from step 5, plus briefing notes - and integration points for this ticket.", - activeForm: "Working on #{id}: {short_title}" -) -``` - -After creating all tasks, mirror the dependency chain from the briefing -using `TaskUpdate` with `addBlockedBy`. - -#### 8d. Spawn agents - -For each agent from the `**Agents:**` line, spawn a teammate in the -background. Spawn all agents in parallel (one message, multiple Task calls): - -**Model pin (MANDATORY for team members):** every team-mode spawn — -i.e. any `Task` with a `team_name` argument — must pass `model: "sonnet"`. -Sprint 37 observed Opus 4.7 teammates ignoring scope rules, leaving -tasks half-done, and failing to report back via SendMessage. Sonnet 4.6 -follows literal rules block discipline better. The **team lead** -(this session, running `/sprint-start`) stays on whatever model the -user has selected — typically Opus. - -**Inline (non-team) Agent spawns are exempt.** One-shot reviewers -(`/pr-review`), research subagents, and other `Task` calls without a -`team_name` keep their default model. The pin applies to the -long-running team-coordination path specifically, not every Agent call. - -``` -Task( - subagent_type: "{name_lowercase}", - team_name: "sprint-{N}-{team}", - name: "{name_lowercase}", - model: "sonnet", - prompt: "You are on the {team} team for Sprint {N}. - Branch: `sprint-{N}/{team}` - - RULES (NON-NEGOTIABLE): - - 0. TEAM SCOPE: Your team is `{team}` on branch `sprint-{N}/{team}`. - Stay within your team's file scope (server → server/, client → client/, copy → wiki/ + docs/atlas/ + content/). - You may read (but not modify): docs/, decisions/, wiki/, .claude/ - Do NOT modify files belonging to other teams. - - 1. GIT: Do NOT run any git commands (commit, push, pull, merge, - checkout, branch, stash, tag, etc.). All git operations are - handled by the team lead. No exceptions. - - 2. DB SCRIPTS: When calling ticket/sprint/sqlite scripts, use - the exact command with no wrappers or chaining. Examples: - tooling/db/ticket show 528 - tooling/db/ticket list --sprint {N} - Do NOT prepend python3, do NOT chain with && or ;, do NOT - add cleanup commands. Just the bare command. - - 3. READ BEFORE WRITE: Before modifying ANY file, Read it first. - Before creating a new file, Glob for similar files to learn - the existing patterns (naming, structure, imports). Follow - the conventions you find — do not invent new ones. - - 4. VERIFY AFTER WRITE: After implementing a change, grep for - all references to functions/properties/classes you modified - or removed. If you renamed, moved, or deleted something, - update EVERY call site. Missing a call site breaks tests - and blocks the team. - - 5. NO PARTIAL WORK: Do not mark a task completed unless ALL - parts of the ticket are implemented. If the ticket says - 'deliver A, B, and C', all three must exist and work. If - you cannot complete part of a task, message the team lead - explaining what is blocked and what remains — do NOT mark - it completed. - - 6. MESSAGE WHEN BLOCKED: If you hit a problem you cannot solve - in 3 attempts, stop and message the team lead immediately. - Do not silently skip work or leave stubs. Do not move to - the next task while the current one is incomplete. - - 7. BACKWARD COMPATIBILITY: When extracting, moving, or - refactoring code, ensure all existing consumers still work. - Add proxy methods/properties if needed. Grep for the old - name to find every call site. - - WORKFLOW: - - 1. Read the sprint briefing: docs/sprints/sprint-{N}/{team}.md - 2. Read the decision files referenced in the briefing. - If your work touches systems.db sources (markers.json, TOML files, - or generator code), read .claude/rules/asset-pipeline.md before - modifying anything. - 3. Check TaskList for available work. - 4. Claim an unblocked task (TaskUpdate with owner: your name), - mark it in_progress, and implement it. - 5. Before marking done, verify: - - All deliverables from the ticket exist (not just some) - - No broken references (grep for changed names/signatures) - - New files follow existing naming and directory conventions - - Modified files still parse (no syntax errors) - 6. Mark the task completed and check TaskList for the next - available task. - 7. If no tasks remain, message the team lead. Do NOT shut down - on your own. - - Use `tooling/db/ticket show <id>` for full ticket specs.", - description: "Sprint {N} {team}: {name}", - run_in_background: true -) -``` - -#### 8d-planning. Planning team variant — proposers vs executors - -When the team is `planning`, agents work in discussion mode, not -implementation mode. The briefing typically defines discussion rounds -(propose → review → execute). Most agents are **proposers** — they -analyze and recommend. Only designated agents **execute** (write to -files, modify DB). - -**Classify agents from the briefing:** -- **Executor agents:** SI (project manager), Qatux (documenter). - These agents wait for consensus before writing anything. -- **Proposer agents:** Everyone else (Gestalt, Tyre, Paula, Ozzie, - etc.). These agents analyze, propose, and discuss — they do NOT - write decision files, modify the DB, or create/delete tickets. - -**Add this block to proposer agent prompts** (replaces rules 3–5 -and the WORKFLOW section from the standard template): - -``` - PLANNING TEAM RULES (replace standard rules 3-7 and WORKFLOW): - - 3. PROPOSE ONLY: You are a discussion participant. Your job is - to ANALYZE tickets, PROPOSE team assignments, and RECOMMEND - splits. You do NOT: - - Write or modify decision files (decisions/*.md) - - Run DB update/insert/delete commands - - Create, delete, or modify tickets - - Mark tasks as completed - Those actions belong to executor agents (SI, Qatux) after - the team lead confirms consensus. - - 4. OUTPUT FORMAT: Your first message to the team lead should - be your full analysis. Structure it as: - - Your position on open questions (with rationale) - - Proposed team assignments (table format) - - Split candidates (if any) - - Sprint readiness assessment - Do NOT claim or work tasks. The team lead coordinates. - - 5. WAIT FOR CONSENSUS: Do not treat your own proposal as - decided. Other agents may disagree. The team lead calls - consensus and directs executors to implement it. -``` - -**Add this block to executor agent prompts** (appended after the -standard rules): - -``` - PLANNING TEAM RULES (additional): - - 8. WAIT FOR CONSENSUS: Do NOT execute DB changes, write - decision files, or create tickets until the team lead - explicitly tells you to. Your first message should confirm - you are ready and describe your execution plan. Then wait. - - 9. EXECUTE EXACTLY WHAT IS DIRECTED: When the team lead sends - you a list of changes, execute them precisely. Do not add - extra changes, reinterpret the instructions, or fill gaps - with your own judgment. If something is ambiguous, ask - before executing. -``` - -#### 8e. Report - -Output to the user: -- Team name: `sprint-{N}-{team}` -- Agents spawned (names and roles) -- Tasks created (count actionable vs blocked) -- How to interact: `SendMessage` to talk to agents, `TaskList` to - check progress - -You are now the team lead. Agents work autonomously — monitor via -`TaskList`, communicate via `SendMessage`, and handle blockers as -they arise. - -**When all tasks complete:** Do NOT shut down agents. The team stays -alive through PR review AND merge. Follow step 9 (post-work lifecycle). - -### 9. Post-work lifecycle - -When all tasks are complete (TaskList shows all completed): - -#### 9a. Commit and push - -Run `/git-commit` to commit all changes, then `/pr-push` to create or -update the PR. Do NOT shut down agents — the team stays alive through -review and merge. - -#### 9b. Wait for review - -Do NOT run `/pr-review` from the team window — PR reviews run from -the `main` branch (a separate window/session). The team window stays -on its sprint branch. - -After pushing and creating the PR, report the PR number to the user -and stop. Wait for review feedback to arrive (the user or the main -session will relay it, or it will appear as Gitea PR comments). - -#### 9c. Handle review outcome - -When review feedback arrives (from the user, main session, or PR -comments): - -**If CHANGES_REQUESTED:** - -1. Parse the review comment table (from the Gitea PR comment or the - review output). Extract each issue with: - - File path and approximate line - - Description - -2. Create a task per issue: - ``` - TaskCreate( - subject: "Review: {short description}", - description: "{full issue description from review table, including - file path and reviewer name}", - activeForm: "Fixing review comment: {short description}" - ) - ``` - Every comment is actionable — there is no "suggestion" tier to skip (per pr-review policy). - -3. Dispatch to idle agents: send each a message via SendMessage telling - them to check TaskList for new review-fix tasks. Agents claim and - work tasks as usual. - -4. After all review-fix tasks are complete, re-run `/git-commit` then - `/pr-push` to update the PR. Then re-run `/pr-review`. - -5. Repeat this loop until review returns APPROVED. - -**If APPROVED (but not yet merged):** - -Do NOT shut down. Approval alone is not terminal — reviewers can leave -follow-up comments, the PR can be re-reviewed, or merge conflicts can -surface. Keep the team alive and idle until the PR is merged into main. - -1. Report to the user: "Sprint {N} {team} PR #{X} approved. Awaiting - merge. Team remains alive." -2. Agents stay idle. Do not reassign them to unrelated work. -3. Periodically check merge state (or wait for the user to confirm the - merge). The `main` session handles the merge itself. -4. If new review comments arrive between approval and merge, treat it - as CHANGES_REQUESTED and re-enter the fix loop. -5. Once the PR is merged, proceed to 9d. - -#### 9d. Handle merge completion - -When the PR is confirmed merged into main (user confirmation, Gitea -state change, or the `main` session reports the merge): - -1. Send `shutdown_request` to all sprint agents. -2. Wait for all `shutdown_response` confirmations. -3. Call `TeamDelete` to clean up. -4. Report: "Sprint {N} {team} complete. PR #{X} merged into main. Team - shut down." diff --git a/.claude/skills/sprint-start/scripts/sprint-teardown.sh b/.claude/skills/sprint-start/scripts/sprint-teardown.sh deleted file mode 100755 index bd7d74201..000000000 --- a/.claude/skills/sprint-start/scripts/sprint-teardown.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -# Clean up ephemeral worktrees for a closed sprint. -# Usage: sprint-teardown.sh <sprint-number> -# -# Removes all worktrees under .sprint/sprint-{N}/ and prunes git metadata. -# Safe to run multiple times — skips already-removed worktrees. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" -SPRINT_BASE="$(dirname "$REPO_ROOT")/.sprint" -SPRINT=${1:?Usage: sprint-teardown.sh <sprint-number>} - -SPRINT_DIR="$SPRINT_BASE/sprint-${SPRINT}" - -if [ ! -d "$SPRINT_DIR" ]; then - echo "No worktrees found for sprint-${SPRINT} (directory $SPRINT_DIR does not exist)." - exit 0 -fi - -echo "Cleaning sprint-${SPRINT} worktrees..." - -for wt in "$SPRINT_DIR"/*/; do - [ -d "$wt" ] || continue - team="$(basename "$wt")" - echo " Removing: $team" - git -C "$REPO_ROOT" worktree remove "$wt" --force 2>/dev/null || echo " (already removed or dirty)" -done - -rmdir "$SPRINT_DIR" 2>/dev/null || true -git -C "$REPO_ROOT" worktree prune - -echo "Done. Sprint-${SPRINT} worktrees cleaned." diff --git a/.claude/skills/sprint-start/scripts/start-sprint.sh b/.claude/skills/sprint-start/scripts/start-sprint.sh deleted file mode 100755 index d72c34d81..000000000 --- a/.claude/skills/sprint-start/scripts/start-sprint.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash -# Opens Ptyxis tabs for the active sprint teams in the CURRENT window. -# Auto-starts Claude Code in each tab via tmux. -# -# Usage: start-sprint.sh [sprint-number] -# If omitted, auto-detects the active sprint from the database. -# -# Assumes the caller is already on main in the current terminal. -# Adds one tab per active team — no duplicate main tab. -# -# Worktrees are created under .sprint/ (ephemeral, cleaned after sprint close). -# Can be called from any directory — resolves paths from the script location. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -# Script lives at .claude/skills/sprint-start/scripts/ — repo root is 4 levels up -REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" -# Parent of repo root is where .sprint/ and the DB live -PARENT="$(dirname "$REPO_ROOT")" - -# Resolve sprint number — argument or active sprint from DB -if [ -n "${1:-}" ]; then - SPRINT="$1" -else - SPRINT=$(cd "$REPO_ROOT" && tooling/db/sqlite-query "SELECT id FROM sprints WHERE status='active'" 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)['rows'][0]['id'])" 2>/dev/null || true) - if [ -z "$SPRINT" ]; then - echo "error: no active sprint found. Pass a sprint number or activate a sprint first." >&2 - exit 1 - fi -fi - -# Query teams with open tickets -TEAMS=$(cd "$REPO_ROOT" && tooling/db/sqlite-query "SELECT DISTINCT team FROM tickets WHERE sprint_id=$SPRINT AND status NOT IN ('done','cancelled') AND team IS NOT NULL" 2>/dev/null | python3 -c "import sys,json; [print(r['team']) for r in json.load(sys.stdin)['rows'] if r['team']]" 2>/dev/null || true) - -if [ -z "$TEAMS" ]; then - echo "Sprint $SPRINT has no open tickets. Opening main only." -fi - -echo "Sprint $SPRINT — teams: ${TEAMS:-none}" - -# ── Open team tabs in the CURRENT window ──────────────────────────── -# No separate main tab — the caller is already on main. -# All team tabs open as --tab in the active Ptyxis window. -for team in $TEAMS; do - BRANCH="sprint-${SPRINT}/${team}" - WDIR="$PARENT/.sprint/sprint-${SPRINT}/${team}" - - # Create worktree if it doesn't exist - if [ ! -d "$WDIR" ]; then - echo "Creating worktree: $WDIR (branch: $BRANCH)" - mkdir -p "$(dirname "$WDIR")" - # Create branch from main if it doesn't exist remotely - if git -C "$REPO_ROOT" rev-parse --verify "origin/$BRANCH" >/dev/null 2>&1; then - git -C "$REPO_ROOT" worktree add "$WDIR" "$BRANCH" - else - git -C "$REPO_ROOT" worktree add -b "$BRANCH" "$WDIR" HEAD - fi - fi - - ptyxis --tab -d "$WDIR" -x 'tmux new-session \; send-keys "claude /sprint-start" Enter' - sleep 0.3 -done - -echo "Session ready. Main + ${TEAMS:-(no teams)}" diff --git a/.claude/skills/sprint-status/SKILL.md b/.claude/skills/sprint-status/SKILL.md deleted file mode 100644 index ada1ccd56..000000000 --- a/.claude/skills/sprint-status/SKILL.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -name: sprint-status -description: > - Sprint health check and cleanup sweep. Lists all tickets in the active - sprint grouped by status, detects bookkeeping issues (stale tickets, - orphan PRs, done-but-open PRs, unassigned work), and shows open work - by team. Use when checking sprint progress, before sprint close, or - when housekeeping feels off. Triggers on "sprint status", "cleanup - sweep", "what's open", "sprint health". -user-invocable: true -allowed-tools: Task, Read, Grep, Glob ---- - -# Sprint Status - -**Delegate this entire skill to a subagent** (general-purpose, model: haiku). - -When this skill is invoked, spawn a subagent using the Task tool: - -``` -Task( - subagent_type: "general-purpose", - model: "haiku", - prompt: "Run /sprint-status. Read the skill at - .claude/skills/sprint-status/SKILL.md for the full workflow - (below the --- separator), then execute it.", - description: "Sprint status report" -) -``` - -Present the subagent's output to the user verbatim. Do NOT run the -workflow yourself. - ---- - -The remainder of this file is the subagent's reference for executing -the workflow. - -## Step 1 — Gather data - -Run these two commands in parallel: - -```bash -tooling/db/sprint sweep -``` - -```bash -tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple -``` - -The `sweep` command returns JSON with: -- `sprint` — id, name, goal -- `progress` — total, done, pct -- `by_status` — tickets grouped into done, review, in_progress, blocked, backlog -- `by_team` — per-team counts -- `issues` — bookkeeping problems with suggested fix commands - -The `tea pr list` returns open PRs as `#N title` lines. - -## Step 2 — Cross-reference PRs with tickets - -Parse PR head branches from the `tea pr list` output. Known team branches: -`server`, `client`, `copy`, `audio`, `visual`, `ci`. - -Detect additional issues: - -- **done_team_open_pr**: A team's tickets are all done but an open PR - still exists for that team branch. -- **orphan_pr**: An open PR exists on a branch that has no tickets in - the active sprint. - -Add these to the issues list from step 1. - -## Step 3 — Format output - -Read `references/output-template.md` for the exact format spec. - -Render the report using data from steps 1-2. Key rules: -- Sections ordered: Completed, In Review, In Progress, Blocked, Backlog -- Sort tickets within sections by team then ticket ID -- Empty sections: show header with "(0)" and "(none)" — no empty table -- Bookkeeping Issues: two-column table (Issue, Fix) -- Open Work by Team: summary table at the bottom -- Issue type labels: `stale_backlog` → "Stale backlog", - `unassigned_in_progress` → "Unassigned in_progress", - `assigned_but_done` → "Assigned but done", - `done_team_open_pr` → "Done team with open PR", - `orphan_pr` → "Orphan PR" - -## Step 4 — Suggest actions - -After the formatted report, if there are bookkeeping issues, add a -"Suggested fixes" section with the fix command for each issue. Group -by issue type for readability. diff --git a/.claude/skills/sprint-status/references/output-template.md b/.claude/skills/sprint-status/references/output-template.md deleted file mode 100644 index 5976c1b39..000000000 --- a/.claude/skills/sprint-status/references/output-template.md +++ /dev/null @@ -1,54 +0,0 @@ -# Sprint Status Output Template - -## Sprint {N}: {Theme} — Status Report - -**Goal:** {goal} -**Status:** {status} | {done}/{total} tickets ({pct}%) -**Open PRs:** {count} ({branches}) - ---- - -### Completed ({count}) - -| # | Team | Title | Assigned | -|---|------|-------|----------| -| #{id} | {team} | {title} | {assigned} | - -### In Review ({count}) - -| # | Team | Title | PR | -|---|------|-------|----| -| #{id} | {team} | {title} | #{pr} | - -### In Progress ({count}) - -| # | Team | Title | Assigned | Note | -|---|------|-------|----------|------| -| #{id} | {team} | {title} | {assigned} | | - -### Blocked ({count}) - -| # | Team | Title | Blocked by | -|---|------|-------|------------| -| #{id} | {team} | {title} | #{ids} | - -### Backlog ({count}) - -| # | Team | Title | Note | -|---|------|-------|----| -| #{id} | {team} | {title} | not started | - ---- - -### Bookkeeping Issues - -| Issue | Fix | -|-------|-----| -| {type}: {detail} | `{command}` | - -### Open Work by Team - -| Team | Backlog | In Progress | Review | Blocked | Done | -|------|---------|-------------|--------|---------|------| -| {team} | {n} | {n} | {n} | {n} | {n} | -| **Total** | **{n}** | **{n}** | **{n}** | **{n}** | **{n}** | diff --git a/.claude/skills/ticket/SKILL.md b/.claude/skills/ticket/SKILL.md index 247191da6..4be1361b8 100644 --- a/.claude/skills/ticket/SKILL.md +++ b/.claude/skills/ticket/SKILL.md @@ -1,9 +1,9 @@ --- name: ticket description: > - Manage project tickets in the SQLite ticketing database. Use when the user - says "ticket", "create a ticket", "show tickets", "sprint", or invokes /ticket. - Wraps the ticket CLI for structured project management operations. + Manage project tickets and milestones in the SQLite ticketing database. Use + when the user says "ticket", "create a ticket", "show tickets", "milestone", + or invokes /ticket. Wraps the ticket CLI for structured project management. user-invocable: true allowed-tools: Bash, Read, Grep, Glob --- @@ -17,12 +17,12 @@ section. This skill covers the full command reference. ### List tickets (full flags) ```bash -tooling/db/ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A] [--team T] +tooling/db/ticket list [--status S] [--priority P] [--epic N] [--milestone N] [--assigned A] [--team T] ``` ### Create ticket ```bash -tooling/db/ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T] +tooling/db/ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T] [--description TEXT] ``` Types: `initiative`, `epic`, `story`, `task`, `bug` Priorities: `critical`, `high`, `medium`, `low` @@ -46,18 +46,27 @@ tooling/db/ticket team <id> <teams> ``` Teams are comma-separated, e.g. `server`, `client`, `server,client`. -### Sprint management -```bash -tooling/db/ticket sprint [--active] -tooling/db/ticket sprint assign <id> <sprint_id> -``` - -For sprint-scoped operations (status overview, context dumps, lifecycle), -use the dedicated sprint CLI instead: `tooling/db/sprint --help` - ### Dependencies ```bash tooling/db/ticket deps <id> +tooling/db/ticket dep add <blocker_id> <blocked_id> +tooling/db/ticket dep rm <blocker_id> <blocked_id> +``` + +### WIP +```bash +tooling/db/ticket wip +``` + +### Milestones +```bash +tooling/db/ticket milestone list [--status S] +tooling/db/ticket milestone create <name> [--description TEXT] [--phase N] +tooling/db/ticket milestone link <ticket_id> <milestone_id> +tooling/db/ticket milestone unlink <ticket_id> <milestone_id> +tooling/db/ticket milestone complete <milestone_id> +tooling/db/ticket milestone show <milestone_id> +tooling/db/ticket milestone dep <blocker_id> <blocked_id> ``` ### Search and browse @@ -75,10 +84,10 @@ tooling/db/ticket show --brief <id> [<id>...] ## Workflow -1. **SI (Project Manager)** is the primary user of this skill -2. Decisions from decisions/ domain files become **initiatives** -3. SI breaks initiatives into **epics** (major work areas) -4. Epics break into **stories** (user-facing deliverables) -5. Stories break into **tasks** (concrete work items assignable to agents) -6. **Sprints** group tasks into time-boxed work periods +1. Decisions from `decisions/` domain files become **initiatives** +2. Initiatives break into **epics** (major work areas) +3. Epics break into **stories** (user-facing deliverables) +4. Stories break into **tasks** (concrete work items) +5. **Milestones** group tickets by deliverable (many-to-many via `milestone link`) +6. `/whats-next` selects the next batch from the dependency graph 7. Track history via `ticket_history` table for audit trail diff --git a/.claude/skills/whats-next/SKILL.md b/.claude/skills/whats-next/SKILL.md new file mode 100644 index 000000000..9bf64ba47 --- /dev/null +++ b/.claude/skills/whats-next/SKILL.md @@ -0,0 +1,209 @@ +--- +name: whats-next +description: > + Analyze the milestone dependency web and surface the best batch of tickets + to pick up next. Refines ticket context via parallel Si agents before + activation. Use when the user says "what's next", "next batch", "pick up + work", or invokes /whats-next. NOT triggered by "what should we work on" + in a design context — that's a discussion, not a batch selection. +user-invocable: true +allowed-tools: Bash, Read, Grep, Glob, Agent, AskUserQuestion +--- + +# What's Next + +Dependency-driven batch selection with ticket refinement. Three steps: +batch selection → refinement review → batch activation. + +## Step 1: Batch Selection + +### 1a. Query active milestones + +```bash +tooling/db/ticket milestone list --status active +``` + +If no active milestones exist, tell the user and stop. + +### 1b. Build the work landscape + +For each active milestone, get its tickets: + +```bash +tooling/db/ticket milestone show <milestone_id> +``` + +Collect all tickets across all active milestones. Deduplicate (a ticket +can appear in multiple milestones). + +### 1c. Identify unblocked tickets + +For each ticket that is `ready` or `backlog`, check its dependencies: + +```bash +tooling/db/ticket deps <id> +``` + +A ticket is **unblocked** if all its blockers are `done` or `cancelled`. +Filter to only unblocked tickets. + +### 1d. Check milestone-level blocking + +```bash +tooling/db/sqlite-query "SELECT * FROM milestone_deps" +``` + +If a milestone is blocked by another milestone that is not yet `completed`, +deprioritize its tickets (they're unblocked at the ticket level but the +milestone isn't ready for focus yet). Still show them, but ranked lower. + +### 1e. Rank and group + +Rank unblocked tickets by: +1. **Priority** (critical > high > medium > low) +2. **Milestone proximity** — milestones closest to completion (highest + done/total ratio) get priority. Finishing a milestone unlocks downstream + milestone deps. +3. **Fan-out** — tickets that unblock the most downstream tickets rank + higher (query `ticket_deps` for blocked_id counts per blocker_id) + +Group into **epic-sized batches**: tickets sharing the same `parent_id` +(epic), or logically related tickets if no epic parent. If a natural +grouping doesn't exist, batch by milestone. + +### 1f. Show WIP status + +```bash +tooling/db/ticket wip +``` + +### 1g. Present the recommended batch + +Show the user: +- The recommended batch (tickets with IDs, titles, priorities, milestones) +- Why this batch (which milestone it advances, what it unblocks) +- Current WIP status +- Any alternative batches worth considering + +Wait for user confirmation before proceeding to Step 2. + +--- + +## Step 2: Ticket Refinement Review + +Spawn one Si agent per ticket in the confirmed batch, running in parallel. +Use `general-purpose` subagent_type with Si's personality baked into the +prompt (custom subagent_types lose SendMessage — see team-test.md). + +For each ticket, spawn: + +``` +Agent({ + subagent_type: "general-purpose", + model: "sonnet", + prompt: "You are SI, the Refinement Manager. <include si.md personality> + + Review ticket #<id>: <title> + Description: <description> + Decision ref: <decision_ref> + + Your job: + 1. Read the ticket's decision_ref D/Q-record in decisions/*.md + 2. Grep for related Q-records in decisions/questions-*.md + 3. Read workshop outcomes if referenced (check docs/workshops/) + 4. Check whether referenced code, tables, or files actually exist + + Assess: does this ticket have enough context for an agent to implement + without guessing? Report exactly one of: + - READY: <one-paragraph summary of what the implementer needs to know> + - GAPS: <list of specific ambiguities with options for each>" +}) +``` + +### 2a. Collect reports + +Wait for all Si agents to complete. Collect their reports. + +### 2b. Resolve gaps + +For any ticket that came back GAPS, surface each ambiguity to the user +via AskUserQuestion: + +> Ticket #NNN: <title> +> Si found these gaps: +> 1. <gap description + options> +> 2. <gap description + options> +> +> How should we resolve these? + +After the user responds, append the resolution to the ticket description: + +```bash +tooling/db/sqlite-exec "UPDATE tickets SET description = description || char(10) || char(10) || '---' || char(10) || 'Refinement: <resolution>' WHERE id = <id>" +``` + +If the user says a gap requires a D-record, flag it and ask whether to +write the D-record now or defer. If deferred, add a note to the ticket. + +### 2c. Present refined batch summary + +Show each ticket with: +- Si's context summary (for READY tickets) or user's resolution (for GAPS) +- Relevant D-record references +- Dependencies + +Ask: "Batch ready. Activate?" + +--- + +## Step 3: Batch Activation + +### 3a. Mark tickets in_progress + +```bash +tooling/db/ticket status <id> in_progress +``` + +For each ticket in the batch. The `status` command will warn if WIP limit +is exceeded. + +### 3b. Create topic branch + +Name the branch after the epic or logical grouping. Examples: +- `tier0-schema-tables` (for a schema epic) +- `heightmap-import` (for a single focused ticket) +- `city-names-pipeline` (for related importer tickets) + +```bash +git checkout -b <branch-name> +``` + +### 3c. Create worktree (optional) + +If the user wants to work in a separate worktree: + +```bash +REPO_ROOT=$(git rev-parse --show-toplevel) +WORKTREE_DIR="$(dirname "$REPO_ROOT")/.worktrees/<branch-name>" +git worktree add "$WORKTREE_DIR" <branch-name> +``` + +Report the worktree path so the user can open a terminal there. + +### 3d. Spawn implementation agents (optional) + +If the user wants agents, spawn them using general-purpose subagent_type +with the relevant personality baked into the prompt. Include: +- The ticket details and Si's context summary +- Relevant D-record content (read and include, don't just reference) +- The RULES block (no git commands, write files only, message when blocked) +- Model: sonnet (per team-mode pin rule) + +### 3e. Report + +Summarize what was activated: +- Branch name +- Worktree path (if created) +- Tickets now in_progress +- Agents spawned (if any) +- Next step: work, then `/pr-process` when ready to ship diff --git a/.config/hooks/pre-push b/.config/hooks/pre-push index 32833554a..592ea2b9d 100755 --- a/.config/hooks/pre-push +++ b/.config/hooks/pre-push @@ -190,6 +190,26 @@ else echo "pre-push: systems.db not in push — skipping stamp check" fi +# --- Clerk review (D-221) --- +# Spawns the clerk agent to check D-record consistency against the diff. +# Optional: only runs if tooling/clerk-review exists and is executable. +# Set SR_SKIP_CLERK=1 to bypass (e.g. for trivial doc-only pushes). +if [ -x "$REPO_ROOT/tooling/clerk-review" ] && [ "${SR_SKIP_CLERK:-0}" != "1" ]; then + echo "pre-push: running clerk review..." + CLERK_VERDICT=$("$REPO_ROOT/tooling/clerk-review" 2>&1 | tee /dev/stderr | tail -1) + if [ "$CLERK_VERDICT" = "APPROVED" ]; then + echo "pre-push: clerk — APPROVED" + elif [ "$CLERK_VERDICT" = "TIMEOUT" ]; then + echo "pre-push: clerk — TIMEOUT (findings may arrive later at .cache/pre-push-review.md)" + ERRORS=$((ERRORS + 1)) + else + echo "pre-push: clerk — REJECTED (see .cache/pre-push-review.md)" + ERRORS=$((ERRORS + 1)) + fi +else + echo "pre-push: clerk review — skipped (not installed or SR_SKIP_CLERK=1)" +fi + if [ "$ERRORS" -gt 0 ]; then echo "" echo "pre-push: $ERRORS check(s) failed. Push aborted." diff --git a/CLAUDE.md b/CLAUDE.md index 316f47472..40fb720d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ A top-down life-sim — asymmetric information, occlusion-based perception, sing **Official Title:** The Settled Reach **Repository name:** settled-reach -**Version source of truth:** `project.yaml` (root `version` field, scheme: `0.1.{sprint_number}`) +**Version source of truth:** `project.yaml` (root `version` field, scheme: `0.{phase}.{n}`) ## Project Structure @@ -13,7 +13,7 @@ client/ # Godot 4 client server/ # Rust/bevy_ecs simulation server tooling/ # Build tools, scripts, asset pipelines tests/ # Integration and end-to-end tests -docs/ # Architecture, design, briefings, sprints, workshops +docs/ # Architecture, design, briefings, workshops db/ # Schema + seed data (connectors moved to tooling/db/) .claude/ # Agents, skills, rules decisions/ # Decision domain files (D-NNN confirmed, Q-NNN open, R-NNN rejected) @@ -48,8 +48,8 @@ Development follows a strict cascade. Each phase has a concrete deliverable. **D ## Work Modes -### Sprint mode (default) -Agents work autonomously on sprint branches. Team lead coordinates via tasks and messages. Human reviews PRs from main. Standard `/sprint-start` → `/pr-push` → `/pr-review` lifecycle. +### Kanban mode (default) +Work flows continuously through milestones. `/whats-next` selects the next epic-sized batch from the dependency graph, refines ticket context via parallel Si agents, then activates a worktree. `/pr-process` (from worktree) and `/pr-review` (from main) handle the review cycle through Gitea PR comments. Milestones are many-to-many with tickets, can run in parallel, and can block other milestones. ### Pair session Human and Claude work together interactively on a single task. No background agents, no autonomous work. Used for load-bearing architecture changes where the human needs to make judgment calls as the work progresses — not approve a finished result. @@ -65,13 +65,10 @@ Human and Claude work together interactively on a single task. No background age ## Agent Instructions -### Team boundaries - -**Your team is determined by your sprint branch** (e.g. `sprint-31/server` → server team). +### Agent scope - All file paths are relative to the repo root (e.g. `server/src/bridge/types.rs`). -- **Stay within your team's scope.** Server team modifies `server/`. Client team modifies `client/`. Copy team modifies `wiki/`, `docs/atlas/`, `content/`. Shared directories (`docs/`, `decisions/`) are readable by all teams. -- **Do NOT modify files outside your team scope** unless the ticket explicitly requires it. +- Topic branches per logical work unit (e.g. `heightmap-import`, `city-names-table`). Any agent can touch any file — personalities guide perspective, not file access. - **Never chain git commands** in a single Bash call (e.g. `git add ... && git commit ...`). Always run `git add` and `git commit` as **separate sequential Bash calls**. - **Stale git lock files:** If a `git` command fails with `index.lock: File exists`, you may remove the lock file at `.git/index.lock` (or `.git/worktrees/<name>/index.lock` if in a worktree). @@ -80,10 +77,11 @@ Human and Claude work together interactively on a single task. No background age The ticketing database (`settledreach.db`) is accessed via `SR_DB_PATH` env var (set in `.claude/settings.json`). A backup is committed to `docs/backups/settledreach.db.backup` via main only. ### Before starting work -1. Read your sprint briefing at `docs/sprints/sprint-N/{team}.md` for current tasks -2. Use `tooling/db/ticket show <id>` for full ticket details -3. Read the relevant `decisions/*.md` domain file(s) referenced in the briefing -4. Background context: `docs/briefings/{your-name}.md`, `docs/discussions/` +1. Check active work: `tooling/db/ticket list --status in_progress` +2. Check your ticket: `tooling/db/ticket show <id>` +3. Check dependencies: `tooling/db/ticket deps <id>` +4. Read relevant `decisions/*.md` domain file(s) referenced in the ticket +5. Background context: `docs/briefings/{your-name}.md`, `docs/discussions/` ### CLI tools @@ -92,7 +90,7 @@ The ticketing database (`settledreach.db`) is accessed via `SR_DB_PATH` env var | Tool | Command | Full reference | |------|---------|----------------| | Tickets | `tooling/db/ticket list`, `show`, `create`, `assign` | `/ticket` skill | -| Sprints | `tooling/db/sprint status`, `start-work`, `prepare` | `/sprint-start` skill | +| Milestones | `tooling/db/ticket milestone list`, `create`, `link`, `show` | `/ticket` skill | | SQL queries | `tooling/db/sqlite-query "SELECT ..."` | — | | SQL writes | `tooling/db/sqlite-exec "UPDATE ..."` | — | | Decisions | `tooling/db/decision show`, `next`, `claim`, `check-dupes` | — | diff --git a/db/schema.sql b/db/schema.sql index f28804a59..2b42c82fa 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -16,19 +16,31 @@ CREATE TABLE IF NOT EXISTS tickets ( assigned_to TEXT, team TEXT, -- comma-separated team names, e.g. 'server', 'client,server' decision_ref TEXT, -- e.g. 'D-010' or 'Q-001' - sprint_id INTEGER REFERENCES sprints(id), created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); -CREATE TABLE IF NOT EXISTS sprints ( +CREATE TABLE IF NOT EXISTS milestones ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, - goal TEXT, - start_date TEXT, - end_date TEXT, - status TEXT NOT NULL DEFAULT 'planning' CHECK(status IN ('planning', 'active', 'completed', 'cancelled')), - created_at TEXT NOT NULL DEFAULT (datetime('now')) + description TEXT, + status TEXT NOT NULL DEFAULT 'active' + CHECK(status IN ('active', 'completed', 'cancelled')), + cascade_phase INTEGER, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + completed_at TEXT +); + +CREATE TABLE IF NOT EXISTS ticket_milestones ( + ticket_id INTEGER NOT NULL REFERENCES tickets(id), + milestone_id INTEGER NOT NULL REFERENCES milestones(id), + PRIMARY KEY (ticket_id, milestone_id) +); + +CREATE TABLE IF NOT EXISTS milestone_deps ( + blocker_id INTEGER NOT NULL REFERENCES milestones(id), + blocked_id INTEGER NOT NULL REFERENCES milestones(id), + PRIMARY KEY (blocker_id, blocked_id) ); CREATE TABLE IF NOT EXISTS ticket_deps ( @@ -56,11 +68,12 @@ CREATE TABLE IF NOT EXISTS ticket_labels ( CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status); CREATE INDEX IF NOT EXISTS idx_tickets_type ON tickets(type); CREATE INDEX IF NOT EXISTS idx_tickets_parent ON tickets(parent_id); -CREATE INDEX IF NOT EXISTS idx_tickets_sprint ON tickets(sprint_id); CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to); CREATE INDEX IF NOT EXISTS idx_tickets_decision ON tickets(decision_ref); CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team); CREATE INDEX IF NOT EXISTS idx_history_ticket ON ticket_history(ticket_id); +CREATE INDEX IF NOT EXISTS idx_ticket_milestones_ticket ON ticket_milestones(ticket_id); +CREATE INDEX IF NOT EXISTS idx_ticket_milestones_milestone ON ticket_milestones(milestone_id); -- --------------------------------------------------------------------------- -- Decision Sync Tables diff --git a/decisions/README.md b/decisions/README.md index 1d7a8e9e6..ec08828c1 100644 --- a/decisions/README.md +++ b/decisions/README.md @@ -10,12 +10,12 @@ Cross-domain decisions live in one file with cross-reference notes in related fi | File | Domain | Decisions | |------|--------|-----------| -| [architecture.md](architecture.md) | Technical foundation | D-008, D-009, D-010, D-012, D-020, D-026, D-030, D-031, D-041, D-042, D-054, D-055, D-066, D-068, D-073, D-085, D-088, D-094, D-096, D-097, D-099, D-100, D-101, D-102, D-103, D-106, D-108, D-109, D-113, D-133, D-134, D-135, D-136, D-137, D-141, D-148, D-149, D-150, D-151, D-152, D-191 | +| [architecture.md](architecture.md) | Technical foundation | D-008, D-009, D-010, D-012, D-020, D-026, D-030, D-031, D-041, D-042, D-054, D-055, D-066, D-068, D-073, D-085, D-088, D-094, D-096, D-097, D-099, D-100, D-101, D-102, D-103, D-106, D-108, D-109, D-113, D-133, D-134, D-135, D-136, D-137, D-141, D-148, D-149, D-150, D-151, D-152, D-188, D-191, D-192, D-194–D-218, D-220 | | [perception.md](perception.md) | Player observation | D-011, D-015, D-016, D-017, D-018, D-019, D-033, D-035, D-043, D-044, D-045, D-046, D-047, D-048, D-049, D-052, D-056, D-057, D-058, D-059, D-060, D-061, D-067, D-069, D-070, D-071, D-072, D-076, D-077, D-078, D-086 | | [content.md](content.md) | NPC, dialogue, templates | D-023, D-024, D-025, D-028, D-029, D-032, D-034, D-035, D-036, D-037, D-050, D-062, D-063, D-064, D-074, D-075, D-084, D-090, D-092, D-093, D-095, D-098, D-104, D-105, D-107, D-121, D-122, D-123, D-124, D-125, D-126, D-127, D-128, D-129, D-130, D-131, D-132, D-138, D-139, D-140, D-142, D-147 | | [scope.md](scope.md) | Game concept, prototype | D-001, D-003, D-005, D-006, D-007, D-013, D-014, D-027, D-038, D-039, D-051, D-053, D-065, D-087, D-089, D-091, D-114, D-115, D-116, D-117, D-118, D-119, D-120, D-145, D-146, D-153, D-154, D-155, D-156, D-157 | | [economics.md](economics.md) | Economics layer, currencies, corporations, simulation | D-171, D-172, D-173, D-174, D-175, D-176, D-177, D-178, D-179, D-180, D-181, D-182, D-183, D-184, D-185, D-186, D-187, D-189, D-190 | -| [process.md](process.md) | Team, workflow | D-004, D-021, D-022, D-040 | +| [process.md](process.md) | Team, workflow | D-004, D-021, D-022, D-040, D-221 | | [questions.md](questions.md) | Open questions (index) | Q-001 through Q-094 | | [questions-architecture.md](questions-architecture.md) | Technical questions | Q-001, Q-006, Q-009, Q-018–Q-023, Q-029, Q-030, Q-046, Q-059, Q-060, Q-063–Q-094 | | [questions-perception.md](questions-perception.md) | Observation questions | Q-003, Q-014, Q-016, Q-024–Q-026, Q-051, Q-053, Q-054 | diff --git a/decisions/process.md b/decisions/process.md index 616e905f1..24aa76ce8 100644 --- a/decisions/process.md +++ b/decisions/process.md @@ -64,4 +64,27 @@ How the team works: composition, naming, workflow. --- -*4 decisions. Last updated: 2026-02-11* +### D-221: Replace sprint workflow with kanban + milestones +- **Date:** 2026-05-03 +- **Decision:** Sprint-based workflow (38 sprints, Sprint 1–38) replaced by kanban + milestones. Sprint infrastructure deleted from the repo — git history serves as the archive. No rollback provision maintained in the codebase. +- **What stays:** Cascade phases (macro direction), agent personalities (divergence engines), D-records, tickets (descriptive not prescriptive), workshops, rules/memory system. +- **What changes:** + - Sprint boundaries → milestones tied to concrete deliverables (cascade phase, feature gate) + - Sprint briefings → live ticket queries (`ticket list --status in_progress` + dependency graph) + - Team branches (`sprint-N/team`) → topic branches per logical work unit + - File-scope restrictions per team → removed; any agent can touch any file + - Version `v0.1.{sprint_number}` → `v0.{phase}.{n}` tied to cascade phases + - Si (project manager) → Si (refinement manager) + new clerk agent (institutional guardrail) + - `/sprint-start` → `/whats-next` (dependency-driven batch selection with parallel Si refinement review) + - `/pr-push` → `/pr-process` (author-side review cycle: commit, push, pick up reviewer comments, dispatch fixes) + - Milestones are many-to-many with tickets, can run in parallel, can block other milestones +- **What dies:** Sprint CLI (`tooling/db/sprint`), sprint skills (`/sprint-start`, `/sprint-plan`, `/sprint-status`), sprint briefing files, sprint close/kickoff ceremonies, team-scoped file restrictions. +- **Rationale:** Structural failure modes across Sprints 37–38: ticket-as-contract (agents execute the letter instead of reading the code), artificial delay (ready work parked for "next sprint"), role theater (file-scope restrictions blocking one-liner fixes), order rigidity (stale briefings), locking on bad design (ticket authority overriding code reality), self-review collapse (coding + reviewing in same context window). See Q-096 for full analysis. +- **Test plan:** First milestone is Tier 0+1 planet-down-cascade work (6 schema tables + 6 Python importers). Measurement: did work flow or stall? Did removing file-scope restrictions cause chaos? Did tickets-as-description work? +- **Source:** Q-096 (raised 2026-04-20, expanded 2026-05-01) +- **Raised by:** Jeroen +- **Dissent:** None + +--- + +*5 decisions. Last updated: 2026-05-03* diff --git a/decisions/questions-process.md b/decisions/questions-process.md new file mode 100644 index 000000000..d1b3e4995 --- /dev/null +++ b/decisions/questions-process.md @@ -0,0 +1,184 @@ +# Open Questions — Process + +How the team works: workflow mechanics, cadence, ceremony, release rhythm. + +--- + +### Q-096: Replace sprint workflow with kanban + milestones + +- **Status:** Resolved — see D-221 +- **Date raised:** 2026-04-20 +- **Last updated:** 2026-05-01 (expanded from "should we?" to concrete proposal with test plan) +- **Raised by:** Jeroen +- **Assigned to:** Jeroen (final call) + +--- + +#### Problem Statement + +Sprints were introduced when the workflow was framed as Scrum-adjacent. Across 38 sprints, the failure modes have sharpened beyond the original "ceremony overhead" concern into structural problems with how agents interpret sprint-shaped work. + +**Original concerns (Sprint 36):** + +| Mechanism | Compensates for | Applies here? | +|---|---|---| +| Time-box (1-2 weeks) | Humans need deadline pressure to scope | No — Claude scopes on demand | +| Retro | Team learning ritual, morale | No — adjustments land as rule/memory edits immediately | +| Burndown / velocity | Estimation & planning for multi-person roadmaps | No — solo dev, no external stakeholders | +| Demoable increments | External stakeholder cadence | No external stakeholders | +| Sprint-kickoff ceremony | Re-energize after context loss | Context loss is per-session, not per-sprint | +| Version bump at sprint close | Predictable shippable cadence | `v0.1.{sprint_number}` doesn't reflect feature progress | + +**New failure modes observed (Sprint 37-38):** + +1. **Ticket-as-contract.** Agents execute the letter of the ticket instead of reading the code and doing what's actually right. If the ticket says "implement X using approach Y," the agent implements Y even when the codebase clearly suggests Z. The ticket becomes a shield against thinking. + +2. **Artificial delay.** Work that's ready gets parked because it's "next sprint." Work that's blocked gets thrashed because it's "this sprint." The sprint boundary creates urgency and delay simultaneously. + +3. **Role theater.** Agents treat team-scope restrictions (server team can't touch client files) as hard boundaries when the fix is a one-liner. This models a human org chart, not the reality of one person and a context window. + +4. **Order rigidity.** Agents unwilling to reorder work when dependencies shift, because the briefing prescribed a sequence. Sprint briefings go stale instantly (Sprint 38 server briefing was missing 26 tickets within a day of creation). + +5. **Locking on bad design.** Agents commit to approaches described in tickets even when implementation reveals the design was wrong. The ticket's authority overrides what the code is telling them. + +6. **Self-review collapse.** Coding agents write code, create the PR, review their own PR, approve it, and push — all in one convergent context. The adversarial review pipeline collapses because the reviewer has all the author's context and biases. PR comments aren't posted, review gates are skipped, the entire point of separate reviewer personalities is defeated. Requires constant human intervention to enforce. + +**Compounding factors (May 2026):** + +- Claude Code team-mode harness partially broken (v2.1.123: custom agent types missing SendMessage, shutdown doesn't work, TeamDelete doesn't kill panes). Workaround exists but fragile. +- Opus 4.7 prompt quality ("floatiness") makes the problem worse — agents are already less precise, and rigid ticket framing doesn't compensate, it amplifies. + +--- + +#### Proposal: Kanban + Milestones + Adversarial Personalities + +**What stays:** + +| Element | Why it stays | +|---------|-------------| +| Cascade phases | Macro direction — what to build in what order | +| Agent personalities + briefings | Adversarial diversity — the real purpose is breaking convergence patterns by seeding different thinking via personality prompts. Not role-play, divergence engines. | +| D-records | Decisions are decisions regardless of process | +| Tickets | Track what needs doing — but descriptive, not prescriptive | +| Workshops | The format where adversarial diversity pays off. Unchanged. | +| Rules/memory | Live institutional knowledge, immediate course correction | + +**What changes:** + +| Old | New | +|-----|-----| +| Sprint numbers, sprint boundaries | Milestones tied to concrete deliverables (cascade phase, feature gate) | +| Sprint briefings (pre-baked, go stale) | Session startup: `ticket list --status in_progress` + blocked-by graph | +| Team branches (sprint-N/server) | Topic branches per logical chunk, any agent can touch any file | +| File-scope restrictions per agent | Removed — personalities guide perspective, not file access | +| Author and reviewer in same session | Worktree isolation enforced: reviewer spawns in separate worktree, reads diff cold | +| Version `v0.1.{sprint_number}` | `v0.{phase}.{n}` tied to cascade phases | +| Sprint-close cleanup sweep | Pre-push clerk hook (automated) + milestone-boundary audit (manual) | +| Si (project manager) + Qatux (librarian) as separate agents | Merged into "clerk" agent — institutional guardrail | +| Sprint velocity tracking | WIP limits per work-type (max N tickets in_progress) | + +**What dies:** + +- Sprint CLI (`tooling/db/sprint`) +- Sprint briefing files (`docs/sprints/sprint-N/`) +- Sprint-kickoff and sprint-close ceremonies +- The Si agent as a standalone personality +- Team-scoped file restrictions in CLAUDE.md + +**Personalities are not teams.** Gestalt, Tyre, Paula, Burnelli-Sheldon, etc. survive as prompt-seeded divergence engines. They make pushback legible — when Jeroen sees a Burnelli-Sheldon objection he knows it's an economics concern, when he sees a Paula objection he knows it's a narrative/political concern. The personality is the signature, not a job description. + +**Worktrees are context firewalls, not team boundaries.** Branches and worktrees survive — but their purpose changes from "team scope" to "context isolation." The reason a coding agent self-reviews its own PR is that it has all the context of writing the code. A reviewer spawned in a separate worktree physically cannot have that context — it reads the diff cold, which is exactly what a reviewer should do. The worktree enforces adversariality at the mechanism level, not the process level. This is an argument FOR keeping branch/worktree separation even after killing team-scoped restrictions. + +--- + +#### Clerk Agent (Si + Qatux merge) + +The team lead (interactive Claude session) optimizes for the user's current intent. This makes it structurally incapable of being the institutional guardrail — it will go along with whatever the user says in the moment, even if it contradicts a decision from three weeks ago. + +The clerk is the separation of concerns between **responsiveness** (team lead) and **institutional memory** (clerk). + +**Pre-push hook implementation:** + +1. Pre-push hook spawns clerk agent with the diff +2. Agent checks D-record consistency, ticket drift, contradiction with active decisions +3. Agent writes findings to `.cache/pre-push-review.md` +4. Agent outputs exactly one word: `APPROVED` or `REJECTED` +5. Hook reads the word, exits 0 or 1 +6. **Timeout behavior:** if agent doesn't respond within N seconds, automatic `REJECTED`. Agent keeps running in background — findings file arrives later. User reads it, decides whether to fix or force-push. + +The findings file separates the agent's reasoning from its authority. Binary gate, verbose reasoning. + +**What the clerk checks:** +- Changed files don't contradict active D-records +- Referenced decisions still exist +- Implementation doesn't drift from ticket intent (but ticket is descriptive, not prescriptive — drift is flagged, not blocked) +- Q-records relevant to changed files are surfaced + +**What the clerk does NOT do:** +- Subjective code quality review (that's /pr-review) +- Sprint management (no sprints) +- Briefing maintenance (briefings replaced by live ticket queries) + +--- + +#### Test Run Plan + +**Scope:** The Tier 0/Tier 1 cascade work — all schema tables and Python importers from the planet-down-cascade workshop. + +**Milestone definition:** "All schema tables and Python importers land on main. `make regen-db` produces a DB with heightmaps, city names, province boundaries, body_radius_km, normalized economic_role." + +**Rules for the test run:** +- No sprint number assigned +- Topic branches per logical chunk (not team branches) +- Any agent personality can touch any file +- Tickets describe what needs doing, not how +- WIP limit: 3 tickets in_progress at once +- Milestone complete when all Tier 0 + Tier 1 tickets are done and verified + +**What we're measuring:** +1. Did work flow or stall without sprint pressure? +2. Did removing file-scope restrictions cause chaos or remove friction? +3. Did tickets-as-description work, or did agents flail without prescriptive tickets? +4. Did anything get worse compared to Sprint 38's current state? + +**Rollback trigger:** If after the test run the work feels less productive or less controlled than a sprint, Sprint 39 picks up where 38 left off. All sprint tooling remains in the codebase — dead code, not deleted code. + +**Parallel work (independent of test run outcome):** +- Build the clerk pre-push hook — pure additive, helps regardless of process +- Can be tested against Sprint 38 pushes before the process switch + +--- + +#### Risks + +| Risk | Mitigation | +|------|-----------| +| No natural "stop and audit" moment | Clerk pre-push hook + milestone-boundary manual audit | +| Work sprawl without sprint focus | WIP limits enforce focus; milestones enforce shipping | +| Losing the "signature" on who did what | Personalities preserved — commits and PRs still carry agent identity | +| Process change is a distraction from actual development | Test run uses real cascade work, not meta-work. Process change is the test. | +| Can't roll back cleanly | Sprint tooling stays in repo. Reversible. | +| Clerk agent too slow / too noisy | Timeout-reject for speed. Tune signal-to-noise over first milestone. | + +--- + +#### Decision Timeline + +1. **Now:** Sprint 38 continues as-is +2. **Sprint 38 close:** Decision point — run test milestone or Sprint 39 +3. **During test milestone:** Build clerk pre-push hook in parallel +4. **Test milestone complete:** Evaluate against measurement criteria +5. **If positive:** Write D-record confirming kanban + milestones. Kill sprint tooling. +6. **If negative:** Sprint 39. Revisit after more Claude Code harness fixes. + +--- + +**Related:** +- `feedback_broken_tests_not_preexisting_shield.md` — pattern sprints may enable +- `team-test.md` — Claude Code harness test results (tmux teams partially broken v2.1.123) +- Epic #854 — pipeline discipline automation +- D-166 — cascade phases + +--- + +*Open count: 0.* diff --git a/docs/briefings/araminta.md b/docs/briefings/araminta.md index 06bb92455..1e9009709 100644 --- a/docs/briefings/araminta.md +++ b/docs/briefings/araminta.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned araminta` ## v0.2 Pivot — Key Changes for Your Role The visual system shifts from designing layouts for three hand-crafted social sites to designing the visual grammar that the generator uses to produce legible zones at scale. diff --git a/docs/briefings/burnelli-sheldon.md b/docs/briefings/burnelli-sheldon.md index b839b0697..44c84297c 100644 --- a/docs/briefings/burnelli-sheldon.md +++ b/docs/briefings/burnelli-sheldon.md @@ -11,7 +11,8 @@ updated: 2026-04-05 Last updated: 2026-04-05 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned burnelli-sheldon` ## Your Domain diff --git a/docs/briefings/clerk.md b/docs/briefings/clerk.md new file mode 100644 index 000000000..aed461a36 --- /dev/null +++ b/docs/briefings/clerk.md @@ -0,0 +1,42 @@ +--- +title: "Clerk - Project Briefing" +description: "Pre-push institutional guardrail — checks D-record consistency, ticket drift, decision contradictions, and Q-record surfacing before code lands" +type: briefing +status: active +agent: Clerk +updated: 2026-05-03 +--- + +# Clerk - Project Briefing +Last updated: 2026-05-03 + +## Role: Pre-Push Institutional Guardrail + +Review branch changes before push to catch institutional drift: decision contradictions, ticket scope creep, missing D-records for new behavior, and unresolved Q-records that should block merge. + +## What to Check + +1. **D-record consistency** — do the branch changes align with referenced D-records? Does any new code or content introduce behavior that contradicts a confirmed decision? +2. **Ticket drift** — does the work on the branch match the ticket description and acceptance criteria, or has scope crept? +3. **Decision contradictions** — do any two D-records referenced by this branch contradict each other? Has a D-record been amended elsewhere but not reflected in the branch? +4. **Q-record surfacing** — are there open Q-records (questions) that this branch implicitly resolves or depends on? If so, flag them for explicit resolution before merge. + +## Where to Find Decisions + +- **Decision index:** `decisions/README.md` — master index of all domain files +- **Domain files:** `decisions/architecture.md`, `decisions/perception.md`, `decisions/content.md`, `decisions/scope.md`, `decisions/economics.md`, `decisions/process.md` +- **Open questions:** `decisions/questions.md`, `decisions/questions-process.md` +- **Rejected proposals:** `decisions/rejected.md` +- **Decision DB table:** + ```bash + tooling/db/sqlite-query "SELECT id, title, status, domain FROM decisions WHERE domain = '<domain>'" + ``` + +## Output + +Write findings to `.cache/pre-push-review.md`. End with a binary verdict: + +- **APPROVED** — no institutional issues found, safe to push +- **REJECTED** — list each issue with the D/Q-record reference and what needs resolution + +The findings file is gitignored (lives in `.cache/`) and is consumed by the push workflow. diff --git a/docs/briefings/dudley.md b/docs/briefings/dudley.md index 4d0b53f0f..6cbe6e7f3 100644 --- a/docs/briefings/dudley.md +++ b/docs/briefings/dudley.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned dudley` ## Status STANDBY. This briefing will be populated when backend/engine implementation begins. Dudley activates when simulation implementation work is assigned. diff --git a/docs/briefings/gestalt.md b/docs/briefings/gestalt.md index be6034b9f..9b148f76e 100644 --- a/docs/briefings/gestalt.md +++ b/docs/briefings/gestalt.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned gestalt` ## Key Design Pivots - **Tycoon bookmark (D-117):** Zero investigation content. Tycoon (small business owner) is the starting state. diff --git a/docs/briefings/gore.md b/docs/briefings/gore.md index 9d8c54510..85f7b4c31 100644 --- a/docs/briefings/gore.md +++ b/docs/briefings/gore.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned gore` ## v0.2 Pivot — Key Changes for Your Role The workshop confirmed and sharpened the thematic frame. "Is this enough?" survives as the central question. The change is that it now must be posed through a generated tycoon's life, not through authored detective/smuggler characters. diff --git a/docs/briefings/hoshe.md b/docs/briefings/hoshe.md index 3595fcfac..c88611281 100644 --- a/docs/briefings/hoshe.md +++ b/docs/briefings/hoshe.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned hoshe` ## Status **ACTIVE.** Test infrastructure from earlier sprints is operational. v0.2 focus: generator output validation — can the generator produce legible NPCs? Are relationships readable from generator output? diff --git a/docs/briefings/justine.md b/docs/briefings/justine.md index ead9a4bdc..27e7cbe98 100644 --- a/docs/briefings/justine.md +++ b/docs/briefings/justine.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned justine` ## Status **ACTIVE.** CI and build pipeline maintenance. Content validation infrastructure remains relevant as the AI-assisted content pipeline (D-123) produces NPC content at scale. diff --git a/docs/briefings/mellanie.md b/docs/briefings/mellanie.md index 580f8f30b..653c7e38c 100644 --- a/docs/briefings/mellanie.md +++ b/docs/briefings/mellanie.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned mellanie` ## v0.2 Pivot — Key Changes for Your Role The copy pipeline fundamentally shifts from hand-authoring specific NPC profiles to designing the culture-driven voice system that feeds the generator. diff --git a/docs/briefings/miri.md b/docs/briefings/miri.md index f60118521..f7dbfcdc1 100644 --- a/docs/briefings/miri.md +++ b/docs/briefings/miri.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned miri` ## v0.2 Pivot — Key Changes for Your Role The workshop confirmed Sova Transit District as the v0.2 setting (tycoon bookmark in Van Maanen's Star) and resolved the culture architecture gap in Miri's favour. diff --git a/docs/briefings/nigel.md b/docs/briefings/nigel.md index 832268aa4..c56c5c434 100644 --- a/docs/briefings/nigel.md +++ b/docs/briefings/nigel.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned nigel` ## v0.2 Pivot — Key Changes for Your Role The replayability frame shifts from "same conspiracy, different character lens" to "same generator, different seeded world." The anti-metagaming goal survives; the mechanism changes. diff --git a/docs/briefings/oscar.md b/docs/briefings/oscar.md index a0605506a..b244096e6 100644 --- a/docs/briefings/oscar.md +++ b/docs/briefings/oscar.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned oscar` ## Status STANDBY. This briefing will be populated when networking/multiplayer implementation begins. diff --git a/docs/briefings/ozzie.md b/docs/briefings/ozzie.md index eb174ab40..25e2c3041 100644 --- a/docs/briefings/ozzie.md +++ b/docs/briefings/ozzie.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned ozzie` ## v0.2 Pivot — Key Changes for Your Role The workshop revealed a core framing error: the game was built as a detective puzzle game but the vision is a single-character life sim. The fix is not cosmetic. diff --git a/docs/briefings/paula.md b/docs/briefings/paula.md index 94efd270b..da31c7e52 100644 --- a/docs/briefings/paula.md +++ b/docs/briefings/paula.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned paula` ## v0.2 Pivot — Key Changes for Your Role diff --git a/docs/briefings/qatux.md b/docs/briefings/qatux.md index 1881effd5..afb8f9df5 100644 --- a/docs/briefings/qatux.md +++ b/docs/briefings/qatux.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned qatux` ## Decisions Relevant to Your Role All decisions (D-001 through D-137) — maintains decision domain files in `decisions/` as the canonical record. Indexes and retrieves decision context for the team. diff --git a/docs/briefings/si.md b/docs/briefings/si.md index 66c497e30..3f570d4f4 100644 --- a/docs/briefings/si.md +++ b/docs/briefings/si.md @@ -1,66 +1,62 @@ --- title: "Si - Project Briefing" -description: "Sprint and project manager — owns sprint planning, ticket tracking, v0.2 critical path dependency chain, and CI pipeline awareness for definition of done" +description: "Refinement manager — assesses ticket readiness, ensures sufficient context before work begins, surfaces missing D/Q-records and unresolved dependencies" type: briefing status: active agent: Si -updated: 2026-03-13 +updated: 2026-05-03 --- # Si - Project Briefing -Last updated: 2026-03-13 +Last updated: 2026-05-03 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned si` + +## Role: Refinement Manager + +Assess whether tickets have enough context to be worked. A ticket is ready when: +- It has a **clear outcome** — the agent can tell when the work is done +- All referenced **D-records are unambiguous** and confirmed (not draft, not contradicted) +- No **conflicting Q-records** remain open that would change the ticket's direction +- All **referenced artifacts exist** (files, schemas, specs mentioned in the ticket or its D-records) + +If any of these conditions fail, the ticket is not ready. Flag what is missing and who owns the resolution. + +## Where to Find Decisions + +- **Decision index:** `decisions/README.md` — lists all domain files and how to query them +- **Domain files:** `decisions/architecture.md`, `decisions/perception.md`, `decisions/content.md`, `decisions/scope.md`, `decisions/economics.md`, `decisions/process.md` +- **Open questions:** `decisions/questions.md` (Q-NNN), `decisions/questions-process.md` (Q-NNN process) +- **Rejected proposals:** `decisions/rejected.md` (R-NNN) +- **Decision DB queries:** + ```bash + tooling/db/decision show D-NNN # Show a specific decision + tooling/db/decision check-dupes "title" # Check for duplicates before creating + ``` + +## Where to Find Workshop Outcomes + +Workshop outputs live at `docs/workshops/<workshop-name>/`: +- `workshop-outcomes.md` — final summary with confirmed D-records +- Round files — per-round notes and participant outputs +- Check these when a ticket references a workshop decision to verify the full context is captured + +## What "Enough Context" Means + +A ticket has enough context when an agent can pick it up cold and deliver without asking clarifying questions. Concretely: + +1. **Outcome is testable** — "implement X" has acceptance criteria; "design Y" has a deliverable format +2. **D-records are stable** — no open Q-record that could reverse the referenced D-record +3. **Dependencies are met** — if the ticket says "blocked by Miri's zone spec," that spec exists or the ticket stays in backlog +4. **Scope is bounded** — the ticket does not silently depend on cascade phases that have not been reached (see D-166 development cascade in CLAUDE.md) + +## Key Commands -## Decisions Relevant to Your Role -All decisions across all domains. Read `decisions/README.md` for the index. Query decisions DB for sprint planning: ```bash -make decisions-coverage # Decision-to-ticket coverage by domain -make decisions-orphan # Decisions without implementing tickets -make decisions-active # List all active decisions +tooling/db/ticket list --status backlog # Candidates for refinement +tooling/db/ticket show <id> # Full ticket details +tooling/db/decision show D-NNN # Check a referenced decision +tooling/db/sqlite-query "SELECT * FROM decisions WHERE status = 'open'" # Open decisions ``` - -## Open Questions Assigned to You -All open questions - tracks as tickets for assignment and progress monitoring. - -## Development Cascade (D-166) -Development follows a strict 6-phase cascade. Currently in Phase 1 (wiki content). Generator spike (D-119) is Phase 5. - -**Prerequisites that must exist before/during the generator spike:** -1. Miri: Zone identity spec + Van Maanen's Star culture profile — blocks voice, NPC gen, AI pipeline -2. Araminta: Zone visual grammar, tile palettes — blocked by Miri zone identity spec -3. Tyre: NpcBlueprint struct design — generator output format -4. Mellanie: Culture-primary voice cards — blocked by Miri culture profile - -**Dependency chain:** -``` -Miri: Zone Identity Spec + Van Maanen's Star Culture Profile - |-- Araminta: zone visual grammar, tile palettes - |-- Tyre: generator zone template parameters, NpcBlueprint - |-- Mellanie: culture-primary voice cards - v -Tyre: Generator Spike (Sprint 25) - |-- Produces: auto-generated location + NPCs - v -Test: Can the player read NPC relationships from generator output? -``` - -**Estimated timeline (Tyre):** 7 sprints to proof-of-life playtest (generated location + legible characters + tycoon bookmark from creation to Day 3). - -## Current Priorities -Consult `docs/sprints/` for current sprint briefing. Break v0.2 decisions (D-114 through D-137) into tickets if not already covered. Track dependencies above. - -## Development Workflow -See `docs/DEVOPS.md` for full procedures. Key awareness for sprint planning: -- `make ci` must pass before any PR merges — definition of done includes CI green -- `make ci-server` = lint-server → build-server → test-server -- `make ci-client` = lint-client → build-client → test-client -- Server work lives in `server/`, client work in `client/`, cross-boundary tests in `tests/` -- Hoshe's test plans target these directories per D-030 testing architecture - -## Key Documents -- decisions/ - domain-split decision files (see decisions/README.md for index) -- docs/DEVOPS.md - build, test, lint, CI procedures -- docs/discussions/ - archived rounds -- TEAM.md - team roster diff --git a/docs/briefings/stig.md b/docs/briefings/stig.md index e9da79fae..360822b0d 100644 --- a/docs/briefings/stig.md +++ b/docs/briefings/stig.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned stig` ## Status **ACTIVE.** v0.2 brings a character customisation UI requirement and removes the dual-character variant for insert/HUD. diff --git a/docs/briefings/tiger.md b/docs/briefings/tiger.md index 5238457fd..f5dc264c7 100644 --- a/docs/briefings/tiger.md +++ b/docs/briefings/tiger.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned tiger` ## Status STANDBY. This briefing will be populated when localization work begins. diff --git a/docs/briefings/troblum.md b/docs/briefings/troblum.md index 1310c2085..4c3187d45 100644 --- a/docs/briefings/troblum.md +++ b/docs/briefings/troblum.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned troblum` ## Decisions Relevant to Your Role - D-010: Architectural baseline - four principles diff --git a/docs/briefings/tyre.md b/docs/briefings/tyre.md index 4a6778d19..fdc4595a5 100644 --- a/docs/briefings/tyre.md +++ b/docs/briefings/tyre.md @@ -11,7 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count). +Check active milestones: `tooling/db/ticket milestone list` +Check your tickets: `tooling/db/ticket list --assigned tyre` ## Decisions Relevant to Your Role Read `decisions/architecture.md` (primary) and `decisions/perception.md` (secondary). Key decisions: diff --git a/tooling/clerk-review b/tooling/clerk-review new file mode 100755 index 000000000..19dd224d9 --- /dev/null +++ b/tooling/clerk-review @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +""" +Clerk pre-push review — spawns the clerk agent to check D-record consistency, +ticket drift, and decision contradictions against the current diff. + +Outputs exactly one word to stdout: APPROVED or REJECTED. +Writes verbose findings to .cache/pre-push-review.md. + +Exit codes: + 0 = APPROVED + 1 = REJECTED (hard contradiction found) + 2 = TIMEOUT (clerk didn't respond in time — treated as REJECTED) +""" + +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +REPO_ROOT = Path(subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], text=True +).strip()) + +CACHE_DIR = REPO_ROOT / ".cache" +FINDINGS_FILE = CACHE_DIR / "pre-push-review.md" +TIMEOUT_SECONDS = 120 + +CLERK_PROMPT = """You are the CLERK, the institutional guardrail for The Settled Reach. + +You are reviewing a pre-push diff for D-record consistency, ticket drift, and decision contradictions. + +## The diff + +{diff} + +## Your task + +1. Read `decisions/README.md` for the domain index. +2. For each changed file in the diff, check: + - Does the change contradict any active D-record? (grep decisions/*.md for relevant keywords) + - If code references a D/Q/R-ID, does that ID exist and is it active? + - If commits reference ticket #NNN, does the implementation match the ticket description? +3. Surface any open Q-records relevant to changed files. + +## Output format + +First, write your detailed findings. Then on the VERY LAST LINE of your response, +output exactly one word — either APPROVED or REJECTED. + +REJECTED only for hard contradictions with active D-records. Everything else +(drift, open Q-records, suggestions) is a finding but not a block. +""" + + +def get_diff(): + """Get the diff that would be pushed.""" + branch = subprocess.check_output( + ["git", "branch", "--show-current"], text=True + ).strip() + + for ref in [f"origin/{branch}", "origin/main"]: + try: + subprocess.check_output( + ["git", "rev-parse", "--verify", ref], + stderr=subprocess.DEVNULL, text=True + ) + return subprocess.check_output( + ["git", "diff", f"{ref}...HEAD"], text=True + ) + except subprocess.CalledProcessError: + continue + + return subprocess.check_output(["git", "diff", "HEAD~1"], text=True) + + +def run_clerk(diff_text): + """Spawn clerk via claude --print, return (verdict, findings).""" + max_diff = 50000 + if len(diff_text) > max_diff: + diff_text = diff_text[:max_diff] + f"\n\n... (truncated, {len(diff_text)} total chars)" + + prompt = CLERK_PROMPT.format(diff=diff_text) + + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f: + f.write(prompt) + prompt_file = f.name + + try: + result = subprocess.run( + [ + "claude", "-p", + "--model", "sonnet", + "--max-turns", "3", + ], + input=prompt, + capture_output=True, + text=True, + timeout=TIMEOUT_SECONDS, + cwd=str(REPO_ROOT), + ) + output = result.stdout.strip() + except subprocess.TimeoutExpired: + return "TIMEOUT", "Clerk agent timed out after {} seconds.".format(TIMEOUT_SECONDS) + finally: + os.unlink(prompt_file) + + if not output: + return "REJECTED", "Clerk agent produced no output." + + lines = output.strip().split("\n") + last_line = lines[-1].strip().upper() + + if last_line == "APPROVED": + verdict = "APPROVED" + elif last_line == "REJECTED": + verdict = "REJECTED" + else: + verdict = "REJECTED" + output += "\n\n(No clear verdict on last line — defaulting to REJECTED)" + + return verdict, output + + +def main(): + CACHE_DIR.mkdir(parents=True, exist_ok=True) + + diff_text = get_diff() + if not diff_text.strip(): + print("APPROVED") + FINDINGS_FILE.write_text("# Clerk Review\n\nNo diff to review.\n\nVerdict: APPROVED\n") + return 0 + + print(" clerk: reviewing diff...", file=sys.stderr) + verdict, findings = run_clerk(diff_text) + + FINDINGS_FILE.write_text(f"# Clerk Pre-Push Review\n\n{findings}\n") + print(f" clerk: verdict — {verdict} (details: {FINDINGS_FILE})", file=sys.stderr) + print(verdict) + + if verdict == "APPROVED": + return 0 + elif verdict == "TIMEOUT": + return 2 + else: + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tooling/db/sprint b/tooling/db/sprint deleted file mode 100755 index 87923c302..000000000 --- a/tooling/db/sprint +++ /dev/null @@ -1,743 +0,0 @@ -#!/usr/bin/env python3 -""" -Sprint CLI — orchestrates sprint lifecycle and context for agents. - -Calls the ticket CLI for data queries (no SQL duplication). -Direct DB access only for sprint lifecycle mutations. - -Usage: - sprint status [--sprint N] [--team T] Sprint progress and ticket overview - sprint sweep [--sprint N] Health check: grouped tickets, issues, team summary (JSON) - sprint start [--sprint N] Activate a planned sprint - sprint stop [--sprint N] Complete an active sprint - sprint start-work [--sprint N] [--team T] Full context dump for starting work - sprint prepare [--sprint N] [--team T] Prepare next sprint (candidates + gaps) -""" - -import json -import subprocess -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -from common import WORKTREE_ROOT, get_connection, load_config # noqa: E402 - -SCRIPT_DIR = Path(__file__).resolve().parent -TICKET_CLI = str(SCRIPT_DIR / "ticket") -PROJECT_ROOT = WORKTREE_ROOT - -REMINDER = """--- -Reminder: Keep ticket status up to date after finishing work. - tooling/db/ticket status <id> in_progress (when starting) - tooling/db/ticket status <id> done (when finished)""" - - -def run_ticket(*args): - """Call the ticket CLI and return parsed JSON.""" - result = subprocess.run( - [sys.executable, TICKET_CLI] + list(args), - capture_output=True, text=True - ) - if result.returncode != 0: - return {"ok": False, "error": result.stderr.strip()} - try: - return json.loads(result.stdout) - except json.JSONDecodeError: - return {"ok": False, "error": f"Bad ticket output: {result.stdout[:200]}"} - - -def parse_flags(args, known_flags): - """Parse --flag value pairs from args, return (flags_dict, positional_args).""" - flags = {} - positional = [] - i = 0 - while i < len(args): - if args[i].startswith("--") and args[i][2:] in known_flags: - key = args[i][2:] - if i + 1 < len(args): - flags[key] = args[i + 1] - i += 2 - else: - positional.append(args[i]) - i += 1 - else: - positional.append(args[i]) - i += 1 - return flags, positional - - -def detect_team(flags): - """Detect team from flags or git branch.""" - if "team" in flags: - return flags["team"] - try: - result = subprocess.run( - ["git", "branch", "--show-current"], - capture_output=True, text=True, cwd=str(PROJECT_ROOT) - ) - branch = result.stdout.strip() - if branch and branch != "main": - return branch - except Exception: - pass - return None - - -def get_all_sprints(): - """Get all sprints via ticket CLI.""" - data = run_ticket("sprint") - if not data.get("ok"): - return [] - return data.get("sprints", []) - - -def detect_sprint(flags, prefer_status=None): - """Detect sprint from flags or by status preference. - - prefer_status: which status to prefer when auto-detecting. - 'active' for status/start-work/stop - 'planning' for start - None for prepare (targets next sprint) - """ - if "sprint" in flags: - sprint_id = int(flags["sprint"]) - sprints = get_all_sprints() - for s in sprints: - if s["id"] == sprint_id: - return s - print(f"Error: Sprint {sprint_id} not found.") - sys.exit(1) - - sprints = get_all_sprints() - if not sprints: - print("Error: No sprints found in database.") - sys.exit(1) - - if prefer_status: - matching = [s for s in sprints if s["status"] == prefer_status] - if len(matching) == 1: - return matching[0] - if len(matching) > 1: - ids = ", ".join(str(s["id"]) for s in matching) - print(f"Error: Multiple {prefer_status} sprints: {ids}. Use --sprint N to specify.") - sys.exit(1) - # Fall through: no match for preferred status - if prefer_status == "active": - # No active sprint - print("Error: No active sprint. Use --sprint N to specify.") - sys.exit(1) - if prefer_status == "planning": - print("Error: No sprint in planning status. Use sprint prepare first.") - sys.exit(1) - - return None - - -def detect_sprint_for_prepare(flags): - """For prepare: target the next sprint after the most recent one.""" - if "sprint" in flags: - sprint_id = int(flags["sprint"]) - sprints = get_all_sprints() - for s in sprints: - if s["id"] == sprint_id: - return s - # Sprint doesn't exist yet — return a stub - return {"id": sprint_id, "status": "new", "name": None} - - sprints = get_all_sprints() - # If there's a planning sprint, use it - planning = [s for s in sprints if s["status"] == "planning"] - if len(planning) == 1: - return planning[0] - if len(planning) > 1: - ids = ", ".join(str(s["id"]) for s in planning) - print(f"Error: Multiple planning sprints: {ids}. Use --sprint N to specify.") - sys.exit(1) - - # Otherwise target max_id + 1 - if sprints: - next_id = max(s["id"] for s in sprints) + 1 - return {"id": next_id, "status": "new", "name": None} - - return {"id": 1, "status": "new", "name": None} - - -def get_tickets_for_sprint(sprint_id, team=None): - """Get tickets for a sprint, optionally filtered by team.""" - args = ["list", "--sprint", str(sprint_id)] - if team: - args += ["--team", team] - data = run_ticket(*args) - if not data.get("ok"): - return [] - return data.get("rows", []) - - -def get_ticket_deps(ticket_id): - """Get dependencies for a ticket.""" - data = run_ticket("deps", str(ticket_id)) - if not data.get("ok"): - return {"blocked_by": [], "blocks": []} - return data - - -def get_ticket_detail(ticket_id): - """Get full ticket detail.""" - data = run_ticket("show", str(ticket_id)) - if not data.get("ok"): - return None - return data.get("ticket") - - -def briefing_path(sprint_id, team): - """Find the briefing file for a sprint/team if it exists.""" - p = PROJECT_ROOT / "docs" / "sprints" / f"sprint-{sprint_id}" / f"{team}.md" - if p.exists(): - return str(p.relative_to(PROJECT_ROOT)) - return None - - -def format_ticket_table(tickets): - """Format tickets as an aligned table.""" - if not tickets: - print(" (none)") - return - # Header - print(f" {'#':<6} {'Title':<50} {'Status':<12} {'Assigned':<10} {'Priority'}") - print(f" {'---':<6} {'---':<50} {'---':<12} {'---':<10} {'---'}") - for t in tickets: - title = t.get("title", "") - if len(title) > 48: - title = title[:45] + "..." - assigned = t.get("assigned_to") or "" - print(f" {t['id']:<6} {title:<50} {t['status']:<12} {assigned:<10} {t['priority']}") - - -# --------------------------------------------------------------------------- -# Commands -# --------------------------------------------------------------------------- - -def cmd_status(args): - flags, _ = parse_flags(args, ["sprint", "team"]) - sprint = detect_sprint(flags, prefer_status="active") - team = detect_team(flags) - - tickets = get_tickets_for_sprint(sprint["id"], team) - - # Header - name = sprint.get("name", f"Sprint {sprint['id']}") - print(f"=== {name} ({sprint['status']}) ===") - if sprint.get("goal"): - print(f"Goal: {sprint['goal']}") - parts = [] - if sprint.get("start_date"): - parts.append(f"Started: {sprint['start_date']}") - if sprint.get("end_date"): - parts.append(f"Ended: {sprint['end_date']}") - if team: - parts.append(f"Team: {team}") - if parts: - print(" | ".join(parts)) - print() - - # Progress - total = len(tickets) - done = sum(1 for t in tickets if t["status"] == "done") - pct = int(done / total * 100) if total > 0 else 0 - print(f"Progress: {done}/{total} done ({pct}%)") - - # Status breakdown - statuses = {} - for t in tickets: - statuses[t["status"]] = statuses.get(t["status"], 0) + 1 - status_parts = [] - for s in ["backlog", "ready", "in_progress", "review", "done", "cancelled"]: - if s in statuses: - status_parts.append(f"{s}: {statuses[s]}") - if status_parts: - print(f" {' | '.join(status_parts)}") - print() - - # Ticket table - print("Tickets:") - format_ticket_table(tickets) - print() - - # Blocked tickets - blocked_lines = [] - for t in tickets: - if t["status"] == "done": - continue - deps = get_ticket_deps(t["id"]) - for b in deps.get("blocked_by", []): - if b["status"] != "done": - blocked_lines.append(f" #{t['id']} blocked by #{b['id']} ({b['status']})") - if blocked_lines: - print("Blocked:") - for line in blocked_lines: - print(line) - print() - - # Briefing - if team: - bp = briefing_path(sprint["id"], team) - if bp: - print(f"Briefing: {bp}") - else: - # Show all available briefings - briefings = [] - for t_name in ["server", "client", "copy", "audio", "visual", "ci", "joint"]: - bp = briefing_path(sprint["id"], t_name) - if bp: - briefings.append(bp) - if briefings: - print("Briefings:") - for bp in briefings: - print(f" {bp}") - - print() - print(REMINDER) - - -def cmd_start(args): - flags, _ = parse_flags(args, ["sprint"]) - sprint = detect_sprint(flags, prefer_status="planning") - - if sprint["status"] != "planning": - print(f"Error: Sprint {sprint['id']} is '{sprint['status']}', expected 'planning'.") - sys.exit(1) - - # Check ticket count - tickets = get_tickets_for_sprint(sprint["id"]) - if not tickets: - print(f"Error: Sprint {sprint['id']} has no tickets. Run sprint prepare first.") - sys.exit(1) - - # Activate - conn = get_connection(load_config()) - conn.execute( - "UPDATE sprints SET status='active', start_date=date('now') WHERE id=?", - (sprint["id"],) - ) - conn.commit() - conn.close() - - # Summary - teams = {} - for t in tickets: - team = t.get("team") or "unassigned" - teams[team] = teams.get(team, 0) + 1 - - name = sprint.get("name", f"Sprint {sprint['id']}") - print(f"Started: {name}") - print(f"Tickets: {len(tickets)}") - for team, count in sorted(teams.items()): - print(f" {team}: {count}") - print() - print(REMINDER) - - -def cmd_stop(args): - flags, _ = parse_flags(args, ["sprint"]) - sprint = detect_sprint(flags, prefer_status="active") - - if sprint["status"] != "active": - print(f"Error: Sprint {sprint['id']} is '{sprint['status']}', expected 'active'.") - sys.exit(1) - - tickets = get_tickets_for_sprint(sprint["id"]) - done = [t for t in tickets if t["status"] == "done"] - cancelled = [t for t in tickets if t["status"] == "cancelled"] - incomplete = [t for t in tickets if t["status"] not in ("done", "cancelled")] - - # Auto-close: mark picked-up tickets (in_progress, review) as done. - # Work merged to main before sprint close means the ticket is done — - # agents just forget to update status. Backlog/ready tickets were - # never started, so they stay as carry-over candidates. - picked_up_statuses = ("in_progress", "review") - picked_up = [t for t in incomplete if t["status"] in picked_up_statuses] - auto_closed = [] - if picked_up: - conn = get_connection(load_config()) - for t in picked_up: - conn.execute("UPDATE tickets SET status='done' WHERE id=?", (t["id"],)) - auto_closed.append(t) - conn.commit() - conn.close() - # Move auto-closed into done count, remove from incomplete - done = done + auto_closed - incomplete = [t for t in incomplete if t["status"] not in picked_up_statuses] - - # Unassign all done tickets in this sprint (agents don't clean up after themselves) - conn = get_connection(load_config()) - conn.execute( - "UPDATE tickets SET assigned_to=NULL WHERE sprint_id=? AND status='done' AND assigned_to IS NOT NULL", - (sprint["id"],) - ) - unassigned = conn.total_changes - conn.commit() - conn.close() - - # Complete the sprint - conn = get_connection(load_config()) - conn.execute( - "UPDATE sprints SET status='completed', end_date=date('now') WHERE id=?", - (sprint["id"],) - ) - conn.commit() - conn.close() - - name = sprint.get("name", f"Sprint {sprint['id']}") - print(f"Completed: {name}") - print(f"Done: {len(done)}/{len(tickets)}") - if cancelled: - print(f"Cancelled: {len(cancelled)}") - if auto_closed: - print(f"Auto-closed: {len(auto_closed)} tickets marked done on sprint close:") - for t in auto_closed: - print(f" #{t['id']}: {t['title']} ({t['status']} → done)") - print() - - if incomplete: - print("Carry-over candidates (never started):") - format_ticket_table(incomplete) - print() - - print(REMINDER) - - -def cmd_start_work(args): - flags, _ = parse_flags(args, ["sprint", "team"]) - sprint = detect_sprint(flags, prefer_status="active") - team = detect_team(flags) - - if sprint["status"] != "active": - print(f"Error: Sprint {sprint['id']} is '{sprint['status']}', expected 'active'.") - sys.exit(1) - - tickets = get_tickets_for_sprint(sprint["id"], team) - - # Header - name = sprint.get("name", f"Sprint {sprint['id']}") - team_label = f" \u2014 {team.title()}" if team else "" - print(f"=== {name}{team_label} ===") - if sprint.get("goal"): - print(f"Goal: {sprint['goal']}") - parts = [f"Status: {sprint['status']}"] - if sprint.get("start_date"): - parts.append(f"Started: {sprint['start_date']}") - print(" | ".join(parts)) - print() - - # Briefing - if team: - bp = briefing_path(sprint["id"], team) - if bp: - print(f"Briefing: {bp}") - # Also check joint briefing - jbp = briefing_path(sprint["id"], "joint") - if jbp: - print(f"Joint briefing: {jbp}") - - # Collect decision refs - decision_refs = set() - for t in tickets: - detail = get_ticket_detail(t["id"]) - if detail and detail.get("decision_ref"): - decision_refs.add(detail["decision_ref"]) - if decision_refs: - print(f"Decisions: {', '.join(sorted(decision_refs))}") - print() - - # Build dependency map - blocked_by_map = {} # ticket_id -> [blocker tickets] - blocks_map = {} # ticket_id -> [blocked ticket ids] - for t in tickets: - deps = get_ticket_deps(t["id"]) - open_blockers = [b for b in deps.get("blocked_by", []) if b["status"] != "done"] - if open_blockers: - blocked_by_map[t["id"]] = open_blockers - blocking = deps.get("blocks", []) - if blocking: - blocks_map[t["id"]] = blocking - - # Categorize - done_tickets = [t for t in tickets if t["status"] == "done"] - blocked_tickets = [t for t in tickets if t["status"] != "done" and t["id"] in blocked_by_map] - actionable_tickets = [t for t in tickets if t["status"] != "done" and t["id"] not in blocked_by_map] - - # Actionable - if actionable_tickets: - print("Actionable (not blocked, not done):") - for t in actionable_tickets: - print(f" #{t['id']}: {t['title']}") - # Metadata line - meta = [t.get("type", ""), f"P:{t['priority']}", f"S:{t['status']}"] - if t.get("assigned_to"): - meta.append(f"@{t['assigned_to']}") - if t.get("team"): - meta.append(f"Team:{t['team']}") - detail = get_ticket_detail(t["id"]) - if detail and detail.get("decision_ref"): - meta.append(f"Ref:{detail['decision_ref']}") - print(f" {' | '.join(meta)}") - if t["id"] in blocks_map: - block_ids = ", ".join(f"#{b['id']}" for b in blocks_map[t["id"]]) - print(f" Blocks: {block_ids}") - print() - - # Blocked - if blocked_tickets: - print("Blocked:") - for t in blocked_tickets: - blockers = blocked_by_map[t["id"]] - blocker_str = ", ".join(f"#{b['id']} ({b['status']})" for b in blockers) - print(f" #{t['id']}: {t['title']} \u2190 blocked by {blocker_str}") - print() - - # Done - if done_tickets: - print("Done:") - for t in done_tickets: - print(f" #{t['id']}: {t['title']} \u2713") - print() - - print(REMINDER) - - -def cmd_sweep(args): - """Health check: grouped tickets, bookkeeping issues, team summary (JSON).""" - flags, _ = parse_flags(args, ["sprint"]) - sprint = detect_sprint(flags, prefer_status="active") - - tickets = get_tickets_for_sprint(sprint["id"]) - - # Build dependency map for blocked detection - blocked_by_map = {} # ticket_id -> [blocker_id, ...] - for t in tickets: - if t["status"] == "done": - continue - deps = get_ticket_deps(t["id"]) - open_blockers = [b["id"] for b in deps.get("blocked_by", []) if b["status"] != "done"] - if open_blockers: - blocked_by_map[t["id"]] = open_blockers - - # Group by status - by_status = {"done": [], "review": [], "in_progress": [], "blocked": [], "backlog": []} - for t in tickets: - entry = { - "id": t["id"], - "title": t["title"], - "team": t.get("team") or "unassigned", - "assigned_to": t.get("assigned_to"), - } - if t["status"] == "done": - by_status["done"].append(entry) - elif t["id"] in blocked_by_map: - entry["blocked_by"] = blocked_by_map[t["id"]] - by_status["blocked"].append(entry) - elif t["status"] == "review": - by_status["review"].append(entry) - elif t["status"] == "in_progress": - by_status["in_progress"].append(entry) - else: - by_status["backlog"].append(entry) - - # Per-team summary - by_team = {} - for status, items in by_status.items(): - for item in items: - team = item["team"] - if team not in by_team: - by_team[team] = {"backlog": 0, "in_progress": 0, "review": 0, "blocked": 0, "done": 0, "total": 0} - by_team[team][status] = by_team[team].get(status, 0) + 1 - by_team[team]["total"] += 1 - - # Bookkeeping issues - issues = [] - for t in tickets: - if t["status"] in ("in_progress", "review") and not t.get("assigned_to"): - issues.append({ - "type": "unassigned_in_progress", - "detail": f"#{t['id']} unassigned {t['status']}", - "fix": f"tooling/db/ticket assign {t['id']} <agent>", - }) - if t["status"] == "backlog" and sprint["status"] == "active" and t["id"] not in blocked_by_map: - issues.append({ - "type": "stale_backlog", - "detail": f"#{t['id']} stale backlog", - "fix": f"tooling/db/ticket status {t['id']} in_progress", - }) - if t["status"] == "done" and t.get("assigned_to"): - issues.append({ - "type": "assigned_but_done", - "detail": f"#{t['id']} done, still assigned", - "fix": f"tooling/db/ticket unassign {t['id']}", - }) - - # Progress - total = len(tickets) - done = len(by_status["done"]) - pct = int(done / total * 100) if total > 0 else 0 - - result = { - "sprint": { - "id": sprint["id"], - "name": sprint.get("name", f"Sprint {sprint['id']}"), - "goal": sprint.get("goal", ""), - }, - "progress": {"total": total, "done": done, "pct": pct}, - "by_status": by_status, - "by_team": by_team, - "issues": issues, - } - print(json.dumps(result, indent=2)) - - -def cmd_prepare(args): - flags, _ = parse_flags(args, ["sprint", "team"]) - sprint = detect_sprint_for_prepare(flags) - team = detect_team(flags) - - # Create sprint record if it doesn't exist - if sprint.get("status") == "new": - conn = get_connection(load_config()) - conn.execute( - "INSERT INTO sprints (id, name, status) VALUES (?, ?, 'planning')", - (sprint["id"], f"Sprint {sprint['id']}") - ) - conn.commit() - conn.close() - print(f"Created Sprint {sprint['id']} (planning)") - sprint["status"] = "planning" - sprint["name"] = f"Sprint {sprint['id']}" - elif sprint["status"] not in ("planning", "new"): - print(f"Warning: Sprint {sprint['id']} is '{sprint['status']}', not 'planning'.") - - print(f"=== Preparing Sprint {sprint['id']} ===") - print() - - # Previous sprint info - all_sprints = get_all_sprints() - prev_sprints = [s for s in all_sprints if s["id"] < sprint["id"]] - if prev_sprints: - prev = max(prev_sprints, key=lambda s: s["id"]) - prev_tickets = get_tickets_for_sprint(prev["id"]) - prev_done = sum(1 for t in prev_tickets if t["status"] == "done") - prev_name = prev.get("name", f"Sprint {prev['id']}") - print(f"Previous: {prev_name} ({prev['status']}, {prev_done}/{len(prev_tickets)} done)") - print() - - # Carry-over candidates - incomplete = [t for t in prev_tickets if t["status"] not in ("done", "cancelled")] - if team: - incomplete = [t for t in incomplete if team in (t.get("team") or "")] - if incomplete: - print("Carry-over candidates (incomplete from previous sprint):") - format_ticket_table(incomplete) - print() - - # Backlog candidates - backlog_args = ["list", "--status", "backlog"] - if team: - backlog_args += ["--team", team] - backlog_data = run_ticket(*backlog_args) - backlog = backlog_data.get("rows", []) if backlog_data.get("ok") else [] - # Filter out tickets already assigned to a sprint - backlog = [t for t in backlog if not t.get("sprint_id")] - - if backlog: - if team: - print(f"Backlog candidates ({team}):") - format_ticket_table(backlog) - else: - # Group by team - by_team = {} - for t in backlog: - t_team = t.get("team") or "unassigned" - by_team.setdefault(t_team, []).append(t) - print("Backlog candidates (unassigned to any sprint):") - for t_name in sorted(by_team.keys()): - print(f"\n {t_name.title()}:") - format_ticket_table(by_team[t_name]) - print() - - # Decision coverage gaps - conn = get_connection(load_config()) - cursor = conn.execute(""" - SELECT id, title FROM decisions - WHERE type='confirmed' AND status='active' - AND id NOT IN (SELECT DISTINCT decision_ref FROM tickets WHERE decision_ref IS NOT NULL) - ORDER BY id - """) - orphans = cursor.fetchall() - conn.close() - - if orphans: - print("Decision coverage gaps (active decisions without tickets):") - for row in orphans: - print(f" {row[0]}: {row[1]}") - print() - - # Already assigned to this sprint - assigned = get_tickets_for_sprint(sprint["id"], team) - if assigned: - print(f"Already assigned to Sprint {sprint['id']}:") - format_ticket_table(assigned) - print() - - print(REMINDER) - - -# --------------------------------------------------------------------------- -# CLI -# --------------------------------------------------------------------------- - -HELP = """sprint \u2014 sprint lifecycle and context for agents - -Usage: - sprint status [--sprint N] [--team T] Sprint progress and ticket overview - sprint sweep [--sprint N] Health check: grouped tickets, issues, team summary (JSON) - sprint start [--sprint N] Activate a planned sprint - sprint stop [--sprint N] Complete an active sprint - sprint start-work [--sprint N] [--team T] Full context dump for starting work - sprint prepare [--sprint N] [--team T] Prepare next sprint (candidates + gaps) - -Sprint auto-detection: - status/start-work/sweep prefer the active sprint - start prefer the planning sprint - stop prefer the active sprint - prepare target next sprint (max id + 1) - -Team auto-detection: - If --team is omitted, uses the current git branch name (unless on main). - On main with no --team, shows all teams.""" - - -def main(): - if len(sys.argv) < 2 or sys.argv[1] in ("--help", "-h", "help"): - print(HELP) - sys.exit(0) - - cmd = sys.argv[1] - args = sys.argv[2:] - - commands = { - "status": cmd_status, - "sweep": cmd_sweep, - "start": cmd_start, - "stop": cmd_stop, - "start-work": cmd_start_work, - "prepare": cmd_prepare, - } - - if cmd not in commands: - print(f"Error: Unknown command '{cmd}'. Use --help for usage.") - sys.exit(1) - - commands[cmd](args) - - -if __name__ == "__main__": - main() diff --git a/tooling/db/ticket b/tooling/db/ticket index b398b0ae9..3c486a5f3 100755 --- a/tooling/db/ticket +++ b/tooling/db/ticket @@ -3,21 +3,29 @@ Ticket CLI — ergonomic interface to the project ticketing database. Usage: - ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A] [--team T] + ticket list [--status S] [--priority P] [--epic N] [--milestone N] [--assigned A] [--team T] ticket show <id> ticket done <id> [<id> ...] ticket status <id> <new_status> ticket assign <id> <agent> ticket unassign <id> ticket team <id> <teams> - ticket sprint [--active] - ticket sprint assign <id> <sprint_id> ticket deps <id> + ticket dep add <blocker_id> <blocked_id> + ticket dep rm <blocker_id> <blocked_id> ticket search <keyword> ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T] [--description TEXT] ticket epics [--status S] ticket children <id> ticket count [--status S] + ticket wip + ticket milestone list [--status S] + ticket milestone create <name> [--description TEXT] [--phase N] + ticket milestone link <ticket_id> <milestone_id> + ticket milestone unlink <ticket_id> <milestone_id> + ticket milestone complete <milestone_id> + ticket milestone show <milestone_id> + ticket milestone dep <blocker_id> <blocked_id> All output is JSON on stdout. """ @@ -30,6 +38,8 @@ sys.path.insert(0, str(Path(__file__).resolve().parent)) from common import get_connection, load_config # noqa: E402 +WIP_LIMIT = 3 + def query(conn, sql, params=()): cursor = conn.execute(sql, params) @@ -72,9 +82,10 @@ def parse_flags(args, known_flags): # --------------------------------------------------------------------------- def cmd_list(conn, args): - flags, _ = parse_flags(args, ["status", "priority", "epic", "sprint", "assigned", "team"]) + flags, _ = parse_flags(args, ["status", "priority", "epic", "milestone", "assigned", "team"]) conditions = [] params = [] + join = "" if "status" in flags: conditions.append("t.status = ?") params.append(flags["status"]) @@ -84,20 +95,20 @@ def cmd_list(conn, args): if "epic" in flags: conditions.append("t.parent_id = ?") params.append(int(flags["epic"])) - if "sprint" in flags: - conditions.append("t.sprint_id = ?") - params.append(int(flags["sprint"])) + if "milestone" in flags: + join = "JOIN ticket_milestones tm ON tm.ticket_id = t.id" + conditions.append("tm.milestone_id = ?") + params.append(int(flags["milestone"])) if "assigned" in flags: conditions.append("t.assigned_to = ?") params.append(flags["assigned"]) if "team" in flags: - # Match exact team name within comma-separated list conditions.append("(',' || t.team || ',' LIKE '%,' || ? || ',%')") params.append(flags["team"]) where = " AND ".join(conditions) if conditions else "1=1" - sql = f"""SELECT t.id, t.type, t.title, t.status, t.priority, t.assigned_to, - t.team, t.parent_id, t.sprint_id - FROM tickets t WHERE {where} + sql = f"""SELECT DISTINCT t.id, t.type, t.title, t.status, t.priority, t.assigned_to, + t.team, t.parent_id + FROM tickets t {join} WHERE {where} ORDER BY CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END, t.id""" @@ -115,19 +126,20 @@ def cmd_show(conn, ids, brief=False): tickets.append({"id": ticket_id, "error": f"Ticket #{ticket_id} not found"}) continue ticket = rows[0] - # Get children children = query(conn, "SELECT id, title, status, priority FROM tickets WHERE parent_id = ? ORDER BY id", (ticket_id,)) - # Get dependencies (what blocks this) blockers = query(conn, """SELECT t.id, t.title, t.status FROM ticket_deps d JOIN tickets t ON d.blocker_id = t.id WHERE d.blocked_id = ?""", (ticket_id,)) - # Get dependents (what this blocks) blocks = query(conn, """SELECT t.id, t.title, t.status FROM ticket_deps d JOIN tickets t ON d.blocked_id = t.id WHERE d.blocker_id = ?""", (ticket_id,)) + milestones = query(conn, """SELECT m.id, m.name, m.status FROM ticket_milestones tm + JOIN milestones m ON tm.milestone_id = m.id + WHERE tm.ticket_id = ?""", (ticket_id,)) ticket["children"] = children ticket["blocked_by"] = blockers ticket["blocks"] = blocks + ticket["milestones"] = milestones tickets.append(ticket) if brief: _print_brief(tickets) @@ -142,9 +154,7 @@ def _print_brief(tickets): if "error" in t: print(f"#{t['id']}: NOT FOUND") continue - # Header line print(f"#{t['id']}: {t['title']}") - # Metadata line parts = [f"{t['type']}", f"P:{t['priority']}", f"S:{t['status']}"] if t.get("assigned_to"): parts.append(f"@{t['assigned_to']}") @@ -152,19 +162,17 @@ def _print_brief(tickets): parts.append(f"Team:{t['team']}") if t.get("parent_id"): parts.append(f"Epic:#{t['parent_id']} ({t.get('parent_title', '?')})") - if t.get("sprint_id"): - parts.append(f"Sprint:{t['sprint_id']}") + if t.get("milestones"): + ms = ", ".join(f"M{m['id']}" for m in t["milestones"]) + parts.append(f"Milestones:{ms}") if t.get("decision_ref"): parts.append(f"Ref:{t['decision_ref']}") print(f" {' | '.join(parts)}") - # Description desc = t.get("description") or "" if desc: - # Truncate long descriptions if len(desc) > 200: desc = desc[:197] + "..." print(f" {desc}") - # Dependencies if t.get("blocked_by"): blockers = ", ".join(f"#{b['id']} ({b['status']})" for b in t["blocked_by"]) print(f" Blocked by: {blockers}") @@ -187,6 +195,11 @@ def cmd_status(conn, ticket_id, new_status): if new_status not in valid: out({"ok": False, "error": f"Invalid status '{new_status}'. Valid: {', '.join(valid)}"}) return + if new_status == 'in_progress': + wip = query(conn, "SELECT COUNT(*) as count FROM tickets WHERE status = 'in_progress'") + count = wip[0]["count"] + if count >= WIP_LIMIT: + print(f"WARNING: WIP limit ({WIP_LIMIT}) reached — {count} tickets already in_progress", file=sys.stderr) updated = execute(conn, "UPDATE tickets SET status=?, updated_at=datetime('now') WHERE id=?", (new_status, int(ticket_id))) out({"ok": True, "updated": updated, "id": int(ticket_id), "status": new_status}) @@ -201,25 +214,6 @@ def cmd_unassign(conn, ticket_id): out({"ok": True, "updated": updated, "id": int(ticket_id), "assigned_to": None}) -def cmd_sprint(conn, args): - flags, positional = parse_flags(args, ["active"]) - if positional and positional[0] == "assign" and len(positional) >= 3: - ticket_id, sprint_id = int(positional[1]), int(positional[2]) - updated = execute(conn, "UPDATE tickets SET sprint_id=?, updated_at=datetime('now') WHERE id=?", (sprint_id, ticket_id)) - out({"ok": True, "updated": updated, "id": ticket_id, "sprint_id": sprint_id}) - return - conditions = [] - params = [] - if "active" in flags: - conditions.append("s.status = 'active'") - where = " AND ".join(conditions) if conditions else "1=1" - sprints = query(conn, f"""SELECT s.*, COUNT(t.id) as ticket_count, - SUM(CASE WHEN t.status='done' THEN 1 ELSE 0 END) as done_count - FROM sprints s LEFT JOIN tickets t ON t.sprint_id = s.id - WHERE {where} GROUP BY s.id ORDER BY s.id DESC""", tuple(params)) - out({"ok": True, "count": len(sprints), "sprints": sprints}) - - def cmd_deps(conn, ticket_id): blockers = query(conn, """SELECT t.id, t.title, t.status, t.priority FROM ticket_deps d JOIN tickets t ON d.blocker_id = t.id @@ -230,6 +224,24 @@ def cmd_deps(conn, ticket_id): out({"ok": True, "id": int(ticket_id), "blocked_by": blockers, "blocks": blocks}) +def cmd_dep(conn, args): + if len(args) < 3: + out({"ok": False, "error": "Usage: ticket dep add|rm <blocker_id> <blocked_id>"}) + return + action, blocker_id, blocked_id = args[0], int(args[1]), int(args[2]) + if action == "add": + try: + execute(conn, "INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (?, ?)", (blocker_id, blocked_id)) + out({"ok": True, "action": "added", "blocker_id": blocker_id, "blocked_id": blocked_id}) + except Exception as e: + out({"ok": False, "error": str(e)}) + elif action == "rm": + updated = execute(conn, "DELETE FROM ticket_deps WHERE blocker_id = ? AND blocked_id = ?", (blocker_id, blocked_id)) + out({"ok": True, "action": "removed", "updated": updated, "blocker_id": blocker_id, "blocked_id": blocked_id}) + else: + out({"ok": False, "error": f"Unknown dep action: {action}. Use 'add' or 'rm'."}) + + def cmd_search(conn, keyword): rows = query(conn, """SELECT id, type, title, status, priority, assigned_to, team FROM tickets WHERE title LIKE ? OR description LIKE ? @@ -295,6 +307,148 @@ def cmd_count(conn, args): out({"ok": True, "rows": rows}) +def cmd_wip(conn): + rows = query(conn, """SELECT id, title, assigned_to, team FROM tickets + WHERE status = 'in_progress' ORDER BY id""") + count = len(rows) + out({"ok": True, "in_progress": count, "limit": WIP_LIMIT, + "over_limit": count > WIP_LIMIT, "tickets": rows}) + + +# --------------------------------------------------------------------------- +# Milestone commands +# --------------------------------------------------------------------------- + +def cmd_milestone(conn, args): + if not args: + out({"ok": False, "error": "Usage: ticket milestone list|create|link|unlink|complete|show|dep ..."}) + return + + sub = args[0] + rest = args[1:] + + if sub == "list": + cmd_milestone_list(conn, rest) + elif sub == "create": + cmd_milestone_create(conn, rest) + elif sub == "link": + cmd_milestone_link(conn, rest) + elif sub == "unlink": + cmd_milestone_unlink(conn, rest) + elif sub == "complete": + cmd_milestone_complete(conn, rest) + elif sub == "show": + cmd_milestone_show(conn, rest) + elif sub == "dep": + cmd_milestone_dep(conn, rest) + else: + out({"ok": False, "error": f"Unknown milestone subcommand: {sub}"}) + + +def cmd_milestone_list(conn, args): + flags, _ = parse_flags(args, ["status"]) + conditions = [] + params = [] + if "status" in flags: + conditions.append("m.status = ?") + params.append(flags["status"]) + where = " AND ".join(conditions) if conditions else "1=1" + rows = query(conn, f"""SELECT m.*, + COUNT(DISTINCT tm.ticket_id) as ticket_count, + SUM(CASE WHEN t.status = 'done' THEN 1 ELSE 0 END) as done_count + FROM milestones m + LEFT JOIN ticket_milestones tm ON tm.milestone_id = m.id + LEFT JOIN tickets t ON t.id = tm.ticket_id + WHERE {where} + GROUP BY m.id ORDER BY m.id""", tuple(params)) + out({"ok": True, "count": len(rows), "milestones": rows}) + + +def cmd_milestone_create(conn, args): + flags, positional = parse_flags(args, ["description", "phase"]) + if not positional: + out({"ok": False, "error": "Usage: ticket milestone create <name> [--description TEXT] [--phase N]"}) + return + name = " ".join(positional) + description = flags.get("description") + phase = int(flags["phase"]) if "phase" in flags else None + conn.execute( + "INSERT INTO milestones (name, description, cascade_phase) VALUES (?, ?, ?)", + (name, description, phase)) + conn.commit() + last_id = query(conn, "SELECT last_insert_rowid() as id")[0]["id"] + out({"ok": True, "id": last_id, "name": name}) + + +def cmd_milestone_link(conn, args): + if len(args) < 2: + out({"ok": False, "error": "Usage: ticket milestone link <ticket_id> <milestone_id>"}) + return + ticket_id, milestone_id = int(args[0]), int(args[1]) + try: + execute(conn, "INSERT INTO ticket_milestones (ticket_id, milestone_id) VALUES (?, ?)", (ticket_id, milestone_id)) + out({"ok": True, "action": "linked", "ticket_id": ticket_id, "milestone_id": milestone_id}) + except Exception as e: + out({"ok": False, "error": str(e)}) + + +def cmd_milestone_unlink(conn, args): + if len(args) < 2: + out({"ok": False, "error": "Usage: ticket milestone unlink <ticket_id> <milestone_id>"}) + return + ticket_id, milestone_id = int(args[0]), int(args[1]) + updated = execute(conn, "DELETE FROM ticket_milestones WHERE ticket_id = ? AND milestone_id = ?", (ticket_id, milestone_id)) + out({"ok": True, "action": "unlinked", "updated": updated, "ticket_id": ticket_id, "milestone_id": milestone_id}) + + +def cmd_milestone_complete(conn, args): + if not args: + out({"ok": False, "error": "Usage: ticket milestone complete <milestone_id>"}) + return + milestone_id = int(args[0]) + updated = execute(conn, "UPDATE milestones SET status='completed', completed_at=datetime('now') WHERE id=?", (milestone_id,)) + out({"ok": True, "updated": updated, "id": milestone_id, "status": "completed"}) + + +def cmd_milestone_show(conn, args): + if not args: + out({"ok": False, "error": "Usage: ticket milestone show <milestone_id>"}) + return + milestone_id = int(args[0]) + ms = query(conn, "SELECT * FROM milestones WHERE id = ?", (milestone_id,)) + if not ms: + out({"ok": False, "error": f"Milestone #{milestone_id} not found"}) + return + milestone = ms[0] + tickets = query(conn, """SELECT t.id, t.type, t.title, t.status, t.priority, t.assigned_to + FROM ticket_milestones tm JOIN tickets t ON t.id = tm.ticket_id + WHERE tm.milestone_id = ? + ORDER BY CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 + WHEN 'medium' THEN 2 ELSE 3 END, t.id""", (milestone_id,)) + blockers = query(conn, """SELECT m.id, m.name, m.status FROM milestone_deps d + JOIN milestones m ON d.blocker_id = m.id + WHERE d.blocked_id = ?""", (milestone_id,)) + blocks = query(conn, """SELECT m.id, m.name, m.status FROM milestone_deps d + JOIN milestones m ON d.blocked_id = m.id + WHERE d.blocker_id = ?""", (milestone_id,)) + milestone["tickets"] = tickets + milestone["blocked_by"] = blockers + milestone["blocks"] = blocks + out({"ok": True, "milestone": milestone}) + + +def cmd_milestone_dep(conn, args): + if len(args) < 2: + out({"ok": False, "error": "Usage: ticket milestone dep <blocker_id> <blocked_id>"}) + return + blocker_id, blocked_id = int(args[0]), int(args[1]) + try: + execute(conn, "INSERT INTO milestone_deps (blocker_id, blocked_id) VALUES (?, ?)", (blocker_id, blocked_id)) + out({"ok": True, "action": "added", "blocker_id": blocker_id, "blocked_id": blocked_id}) + except Exception as e: + out({"ok": False, "error": str(e)}) + + # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- @@ -302,21 +456,29 @@ def cmd_count(conn, args): HELP = """ticket — project ticket CLI Usage: - ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A] [--team T] + ticket list [--status S] [--priority P] [--epic N] [--milestone N] [--assigned A] [--team T] ticket show [--brief] <id> [<id>...] Full ticket detail (--brief for summary) ticket done <id> [<id> ...] Mark tickets as done ticket status <id> <new_status> Change ticket status - ticket assign <id> <agent> Assign ticket to agent/branch + ticket assign <id> <agent> Assign ticket to agent ticket unassign <id> Remove assignment - ticket team <id> <teams> Set team(s) (comma-separated, e.g. server,client) - ticket sprint [--active] List sprints - ticket sprint assign <id> <sprint> Assign ticket to sprint + ticket team <id> <teams> Set team(s) (comma-separated) ticket deps <id> Show ticket dependencies + ticket dep add <blocker> <blocked> Add ticket dependency + ticket dep rm <blocker> <blocked> Remove ticket dependency ticket search <keyword> Search tickets by title/description ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T] [--description TEXT] ticket epics [--status S] List epics with child counts ticket children <id> List children of a ticket - ticket count [--status S] Count tickets by status""" + ticket count [--status S] Count tickets by status + ticket wip Show in-progress count vs WIP limit + ticket milestone list [--status S] List milestones + ticket milestone create <name> [--description TEXT] [--phase N] Create milestone + ticket milestone link <ticket> <milestone> Link ticket to milestone + ticket milestone unlink <ticket> <milestone> Unlink ticket from milestone + ticket milestone complete <milestone> Mark milestone completed + ticket milestone show <milestone> Show milestone with tickets + ticket milestone dep <blocker> <blocked> Add milestone dependency""" def main(): @@ -369,13 +531,13 @@ def main(): out({"ok": False, "error": "Usage: ticket team <id> <teams>"}) else: cmd_team(conn, args[0], args[1]) - elif cmd == "sprint": - cmd_sprint(conn, args) elif cmd == "deps": if not args: out({"ok": False, "error": "Usage: ticket deps <id>"}) else: cmd_deps(conn, args[0]) + elif cmd == "dep": + cmd_dep(conn, args) elif cmd == "search": if not args: out({"ok": False, "error": "Usage: ticket search <keyword>"}) @@ -392,6 +554,10 @@ def main(): cmd_children(conn, args[0]) elif cmd == "count": cmd_count(conn, args) + elif cmd == "wip": + cmd_wip(conn) + elif cmd == "milestone": + cmd_milestone(conn, args) else: out({"ok": False, "error": f"Unknown command: {cmd}. Use --help for usage."}) finally: