From 54eba6163fa97e9f408077fb50140431b5f4e380 Mon Sep 17 00:00:00 2001 From: Topi Jarvinen Date: Wed, 15 Jul 2026 12:29:15 +0300 Subject: [PATCH 1/3] feat: add Codex-compatible workflows --- .agents/skills/parallel/SKILL.md | 14 + .agents/skills/parallel/agents/openai.yaml | 4 + .agents/skills/pr-watch/SKILL.md | 13 + .agents/skills/pr-watch/agents/openai.yaml | 4 + .agents/skills/session-start/SKILL.md | 13 + .../skills/session-start/agents/openai.yaml | 4 + .agents/skills/wrap-up/SKILL.md | 13 + .agents/skills/wrap-up/agents/openai.yaml | 4 + .claude/commands/adopt.md | 22 +- .claude/commands/parallel.md | 422 +----------------- .claude/commands/pr-watch.md | 89 +--- .claude/commands/session-start.md | 176 +------- .claude/commands/wrap-up.md | 79 +--- .claude/rules/safety-critical-changes.md | 45 +- .github/workflows/test.yml | 20 + README.md | 67 ++- config/dev-model.yaml | 46 +- docs/AGENTS-sections.md | 22 + docs/CLAUDE-sections.md | 2 +- .../safety-critical-changes.md | 37 ++ docs/agentic-dev-kit/workflows/parallel.md | 418 +++++++++++++++++ docs/agentic-dev-kit/workflows/pr-watch.md | 98 ++++ .../workflows/session-start.md | 183 ++++++++ docs/agentic-dev-kit/workflows/wrap-up.md | 86 ++++ docs/autonomous-session-playbook.md | 2 +- docs/friction-log-archive.md | 4 + docs/getting-started.md | 33 +- docs/handoff-history.md | 6 + docs/handoff.md | 4 +- docs/parallel-dev.md | 14 +- docs/parallel-howto.md | 43 +- init.sh | 20 +- scripts/archive_plan_sessions.py | 41 +- scripts/dev_session.sh | 120 ++++- scripts/lib/repo_root.sh | 15 + scripts/pr_watch.py | 3 +- scripts/reconcile_sessions.sh | 7 +- scripts/tests/test_portability.py | 162 +++++++ 38 files changed, 1428 insertions(+), 927 deletions(-) create mode 100644 .agents/skills/parallel/SKILL.md create mode 100644 .agents/skills/parallel/agents/openai.yaml create mode 100644 .agents/skills/pr-watch/SKILL.md create mode 100644 .agents/skills/pr-watch/agents/openai.yaml create mode 100644 .agents/skills/session-start/SKILL.md create mode 100644 .agents/skills/session-start/agents/openai.yaml create mode 100644 .agents/skills/wrap-up/SKILL.md create mode 100644 .agents/skills/wrap-up/agents/openai.yaml create mode 100644 .github/workflows/test.yml create mode 100644 docs/AGENTS-sections.md create mode 100644 docs/agentic-dev-kit/safety-critical-changes.md create mode 100644 docs/agentic-dev-kit/workflows/parallel.md create mode 100644 docs/agentic-dev-kit/workflows/pr-watch.md create mode 100644 docs/agentic-dev-kit/workflows/session-start.md create mode 100644 docs/agentic-dev-kit/workflows/wrap-up.md create mode 100644 docs/friction-log-archive.md create mode 100644 docs/handoff-history.md create mode 100644 scripts/lib/repo_root.sh create mode 100644 scripts/tests/test_portability.py diff --git a/.agents/skills/parallel/SKILL.md b/.agents/skills/parallel/SKILL.md new file mode 100644 index 0000000..1877ebf --- /dev/null +++ b/.agents/skills/parallel/SKILL.md @@ -0,0 +1,14 @@ +--- +name: parallel +description: Plan, launch, inspect, reconcile, and remove isolated development lanes backed by Git worktrees and per-lane state sandboxes. Use when work can be split into disjoint file footprints, when the user requests parallel development, or when inspecting existing agent lanes. +--- + +# Parallel Development + +1. Work from the repository root. +2. Read `config/dev-model.yaml` and `docs/agentic-dev-kit/workflows/parallel.md` completely. +3. Follow the requested action. With no action, show the read-only lane board. +4. Resolve engine paths from the repository root; support both `scripts/dev_session.sh` and a namespaced adopted path such as `scripts/devkit/dev_session.sh`. +5. Use the current runtime's supported parallel-task mechanism. Do not assume peer messaging, model selection, background execution, or automatic terminal launch unless the runtime exposes it. +6. Preserve the cockpit/lane ownership boundary and require disjoint source-file footprints before launch. +7. For behavioral changes to lane safety, read and apply `docs/agentic-dev-kit/safety-critical-changes.md`. diff --git a/.agents/skills/parallel/agents/openai.yaml b/.agents/skills/parallel/agents/openai.yaml new file mode 100644 index 0000000..ef8fb25 --- /dev/null +++ b/.agents/skills/parallel/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Parallel Development" + short_description: "Plan and coordinate isolated work lanes" + default_prompt: "Use $parallel to plan or coordinate isolated development lanes with disjoint file ownership." diff --git a/.agents/skills/pr-watch/SKILL.md b/.agents/skills/pr-watch/SKILL.md new file mode 100644 index 0000000..d480376 --- /dev/null +++ b/.agents/skills/pr-watch/SKILL.md @@ -0,0 +1,13 @@ +--- +name: pr-watch +description: Drive a pull request through the deterministic poll, fix, acknowledge, and re-poll loop until CI is green and review findings are resolved. Use after opening or updating a pull request, when asked to watch CI or reviews, or when a task must continue until its PR is green and clean. +--- + +# PR Watch + +1. Work from the repository root. +2. Read `config/dev-model.yaml` and `docs/agentic-dev-kit/workflows/pr-watch.md` completely. +3. Follow the workflow for the PR number in the user's request, or the current branch's PR when none is supplied. +4. Resolve the engine path from the repository root; support both `scripts/pr_watch.py` and a namespaced adopted path such as `scripts/devkit/pr_watch.py`. +5. Use `review.fallback_commands` for the current runtime when configured. Never treat an unavailable review bot as a review waiver. +6. For safety-critical changes, also read and apply `docs/agentic-dev-kit/safety-critical-changes.md` before recommending merge. diff --git a/.agents/skills/pr-watch/agents/openai.yaml b/.agents/skills/pr-watch/agents/openai.yaml new file mode 100644 index 0000000..b67a538 --- /dev/null +++ b/.agents/skills/pr-watch/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "PR Watch" + short_description: "Drive a pull request to green" + default_prompt: "Use $pr-watch to monitor a pull request, address findings, and continue until it is green and clean." diff --git a/.agents/skills/session-start/SKILL.md b/.agents/skills/session-start/SKILL.md new file mode 100644 index 0000000..4afd415 --- /dev/null +++ b/.agents/skills/session-start/SKILL.md @@ -0,0 +1,13 @@ +--- +name: session-start +description: Build a concise start-of-session briefing from the living handoff, friction log, repository state, open pull requests, CI, and configured tracker. Use at the beginning of a development session, when resuming work after a gap, or when asked what the repository should do next. +--- + +# Session Start + +1. Work from the repository root. +2. Read `config/dev-model.yaml` and resolve configured paths from it. +3. Read `docs/agentic-dev-kit/workflows/session-start.md` completely. +4. Follow that workflow using the user's request as additional session context. +5. Use the current runtime's available tools for independent reads and delegation. Treat configured model names as capability guidance; do not claim a model switch the runtime cannot perform. +6. Keep the briefing read-only unless the user explicitly asks to begin the recommended work. diff --git a/.agents/skills/session-start/agents/openai.yaml b/.agents/skills/session-start/agents/openai.yaml new file mode 100644 index 0000000..a2686dc --- /dev/null +++ b/.agents/skills/session-start/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Session Start" + short_description: "Brief the next development session" + default_prompt: "Use $session-start to inspect the repository and recommend the highest-priority next task." diff --git a/.agents/skills/wrap-up/SKILL.md b/.agents/skills/wrap-up/SKILL.md new file mode 100644 index 0000000..2174b5c --- /dev/null +++ b/.agents/skills/wrap-up/SKILL.md @@ -0,0 +1,13 @@ +--- +name: wrap-up +description: Close a meaningful development session by updating the living handoff, capturing workflow friction, checking document budgets, and preserving one clear next step. Use when the user asks to wrap up, hand off, end a session, or record what shipped for the next agent. +--- + +# Wrap Up + +1. Work from the repository root. +2. Read `config/dev-model.yaml` and resolve configured paths from it. +3. Read `docs/agentic-dev-kit/workflows/wrap-up.md` completely. +4. Follow that workflow using the current conversation and repository diff as the session record. +5. Preserve unrelated user changes. Do not claim that work shipped unless the repository and pull-request state support it. +6. Use the current runtime's review and commit mechanisms; require user authorization for external mutations not already requested. diff --git a/.agents/skills/wrap-up/agents/openai.yaml b/.agents/skills/wrap-up/agents/openai.yaml new file mode 100644 index 0000000..2b7289b --- /dev/null +++ b/.agents/skills/wrap-up/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Wrap Up" + short_description: "Close a session and preserve context" + default_prompt: "Use $wrap-up to record what shipped, capture friction, and leave a clear next step." diff --git a/.claude/commands/adopt.md b/.claude/commands/adopt.md index 5ac5990..31bd92c 100644 --- a/.claude/commands/adopt.md +++ b/.claude/commands/adopt.md @@ -3,7 +3,7 @@ the repo lacks, point the config at conventions it already has, and never clobbe file. The counterpart to `init.sh` (which assumes a fresh/near-empty repo). Runs non-destructively on a branch; the operator confirms the plan before anything is written. -> **Why a skill, not `cp -r`.** A blind copy-in clobbers an existing `.claude/`, config, +> **Why a skill, not `cp -r`.** A blind copy-in clobbers existing agent adapters, config, > plan doc, and CI. Adopting into a mature repo is a *judgment* pass — which pieces are > already present, where the scripts should live, what the config should point at — so > it's a guided skill, not a script. @@ -25,7 +25,8 @@ of it from the target repo's root. Run these probes and record the answers — they drive the plan: - **Living plan?** `ls ROADMAP.md PLAN.md docs/plan.md docs/handoff.md handoff.md 2>/dev/null`. If one exists, the repo already practices Principle #1 — you'll point the kit at it, not add a second plan. -- **Skill collisions?** `ls .claude/commands/ 2>/dev/null` — which of the kit's skills (`session-start`, `wrap-up`, `parallel`, `pr-watch`, `triage-friction-log`, `post-merge-systemize`) already exist? Keep theirs; install only the rest. +- **Skill collisions?** Inspect `.claude/commands/` and `.agents/skills/`. Which of the kit's workflows already exist for either runtime? Keep the adopter's implementation and install only the missing adapters. +- **Persistent agent rules?** Read `AGENTS.md` and `CLAUDE.md` when present. Merge the relevant snippets; never replace either file. - **Config dir?** `ls -d config 2>/dev/null` — where `config/dev-model.yaml` goes (repo root if there's no `config/`). - **`scripts/` layout?** `ls scripts 2>/dev/null` — if it's organized into subdirs, or has files that collide with the kit's script names, vendor the kit's under `scripts/devkit/`; otherwise `scripts/` is fine. - **Tracker?** `gh issue list -L1 2>/dev/null` succeeds → GitHub Issues; else look for a Linear/Jira setup. Sets `tracker.backend`. @@ -40,6 +41,7 @@ Present a table the operator confirms **before any write**: |---|---|---| | Living plan (#1) | e.g. has `ROADMAP.md` | **config-point** `paths.handoff` → it; keep it (or offer to rename → `handoff.md`) | | `wrap-up` skill | has its own | **skip** | +| Codex adapters | none | **install** under `.agents/skills/` | | friction-log (#2) | none | **install** | | parallel + `state_paths` (#3) | none | **install** (under `scripts/devkit/` if `scripts/` is organized) | | `pr-watch` (#5), safety rule (#6) | none | **install** | @@ -57,20 +59,21 @@ git checkout -b chore/adopt-agentic-dev-kit For each piece, **copy only if the target doesn't already exist**: -- **Skills** → `.claude/commands/` (skip any that collide with an existing one). -- **Engine scripts** → `scripts/devkit/` (or `scripts/` if clean). If you namespaced them, rewrite the `scripts/…` references in the copied skills to match (`sed -i '' 's#scripts/#scripts/devkit/#g' .claude/commands/.md`). The scripts find the repo root by walking up for `.git`, so they work at any depth. -- **`.claude/rules/safety-critical-changes.md`** (skip if present). -- **`config/dev-model.yaml`** — stamp the Step-1 values: `paths.handoff` → the existing plan (and `paths.handoff_history` / the `doc_budgets` entry to match), `tracker`, `review`, `models`. +- **Shared workflows** → `docs/agentic-dev-kit/workflows/`. +- **Runtime adapters** → `.claude/commands/` and `.agents/skills/` (skip any target that collides with an existing workflow). +- **Engine scripts** → `scripts/devkit/` (or `scripts/` if clean). Set `paths.engines` to that directory; do not rewrite prompt files. The scripts find the repo root by walking up for `.git`, so they work at any depth. +- **Safety doctrine** → `docs/agentic-dev-kit/safety-critical-changes.md`; install the thin `.claude/rules/safety-critical-changes.md` adapter when absent and merge `docs/AGENTS-sections.md` into an existing `AGENTS.md` when applicable. +- **`config/dev-model.yaml`** — stamp the Step-1 values: `paths.handoff` → the existing plan (and `paths.handoff_history` / the `doc_budgets` entry to match), `paths.engines`, `runtime`, `tracker`, `review`, and `models`. - **`friction-log.md`** (seed only if absent). - Append `state/` and `.devkit_state_root` to `.gitignore` if missing. -- Copy `PRINCIPLES.md` + `docs/parallel-dev.md` under `docs/agentic-dev-kit/` for reference. +- Copy `PRINCIPLES.md`, `docs/parallel-dev.md`, and the shared workflow/safety docs under `docs/agentic-dev-kit/` for reference. **Never overwrite an existing file.** If something you didn't anticipate collides, stop and ask the operator. ## Step 4 — Verify -- `state_paths` tests: `cd scripts/devkit/lib && python -m pytest state_paths/tests/ -q` — all pass (proves the sandbox resolves from the new location). +- Portability tests: run `python -m pytest scripts/devkit/lib/state_paths/tests/ scripts/devkit/tests/ -q` (adjust the prefix when engines live directly under `scripts/`). - `check_doc_budget`: run it — it should read the configured plan via `config/dev-model.yaml`. - Confirm the repo's CI/lint scope **skips** the kit files (or add a kit-dir exclude if lint is repo-wide). @@ -84,5 +87,6 @@ issue upstream. This first entry *is* Principle #2 in action. ## Step 6 — Summarize + hand off Report what was **installed / skipped / config-pointed**, open a **draft PR**, and -suggest the operator's first `/session-start`. Leave the merge to the operator — an +suggest the operator's first `/session-start` (Claude) or `$session-start` (Codex). +Leave the merge to the operator — an adoption touches a lot of the repo and deserves a human review pass. diff --git a/.claude/commands/parallel.md b/.claude/commands/parallel.md index aafa66e..57c1ee4 100644 --- a/.claude/commands/parallel.md +++ b/.claude/commands/parallel.md @@ -1,420 +1,4 @@ -Manage isolated **parallel dev sessions** — each one its own git worktree on a fresh -branch plus its own `DEVKIT_STATE_ROOT` sandbox, so several agent/dev sessions run at -once without clobbering each other's checkout or `state/cache/`. Thin in-session -front-end over `scripts/dev_session.sh` (the activation of the state-sandbox -primitive — see Principle #3 in `PRINCIPLES.md`). +Read `docs/agentic-dev-kit/workflows/parallel.md` completely and follow it. -Engine: [`scripts/dev_session.sh`](../../scripts/dev_session.sh). Sessions live in a -sibling `-sessions/` dir by default (override with `DEVKIT_SESSIONS_DIR`); -your CI/cron runner sets neither env var, so it's unaffected. - -## Default action — show the board - -With no argument (or `/parallel list`), run and render the table of active sessions: - -```bash -scripts/dev_session.sh list -``` - -Columns: `SCOPE · BRANCH · PR · CI (✓/✗/…) · DIRTY (uncommitted count) · SANDBOX -path`. This is the orientation surface when you're juggling several sessions — report -it as-is, then stop. Read-only. - -### Live board — `/parallel list --watch [interval]` - -`list` is a one-shot snapshot. To *follow* in-flight lanes — CI flips, commits -landing, the DIRTY count moving, a PR going draft→ready or merging, or a silently-dead -lane that never moves — run the polling board instead: - -```bash -scripts/dev_session.sh list --watch # re-render every 30s -scripts/dev_session.sh list --watch 10 # …or every 10s -``` - -It re-renders on the interval and **marks with a leading `*`** (bold on a TTY) every -row whose state changed since the previous render — the change set is CI ✓/✗/…, a new -commit (HEAD moved), a DIRTY-count change, or a PR-state change (draft↔ready, review -decision). The first frame is the baseline (nothing marked); Ctrl-C stops it. On an -interactive terminal an unbounded watch repaints **in place** on the alternate screen -buffer (like `top`) — each frame replaces the last rather than scrolling a fresh copy -into your terminal history, and your pre-watch screen is restored when you Ctrl-C out -(a board taller than the window is clipped while watching, as with any full-screen -tool — use a taller window). A bounded `--max-iters` run instead leaves its final -frame on screen. The per-row SANDBOX cell is the compact `/state` tail (the -shared sandbox root is named once in the banner) so rows don't wrap on a long -absolute path — **this compaction applies to piped output too**; for the full -absolute sandbox path use bare `list`. Piped/redirected, the board is plain -escape-free text. Each `gh` lookup keeps the same short timeout as bare `list`, so a -slow network caps per-call and never hangs the loop. Use it as the cockpit's ambient -board while a batch is running rather than re-typing `list`. - -## Planning a batch — `/parallel plan [focus]` - -When the operator wants to **start several sessions at once** (or asks "what could we -work on in parallel?"), don't jump to `new` ticket-by-ticket. The suitability test for -parallel work is **disjoint file footprints**: two sessions are safe together only -when no source file is edited by both. The sandbox makes concurrent `state/cache/` -*writes* safe — it does **nothing** for two branches editing the same source file -(that's a merge conflict + diluted review at PR time). So compose the batch -deliberately: - -1. **Orient.** `scripts/dev_session.sh list` + `git worktree list` — active sessions - are file territory already claimed; exclude their footprints from the new batch. - -1. **Gather candidates.** Pull open tickets from your tracker (project - `tracker.project_name`, states In Progress + Todo) and the `▶ Next` deferred items - in [`docs/handoff.md`](../../docs/handoff.md). If a `` argument was given (a - theme, an area, or an explicit ticket list), scope to it. - -1. **Cluster by file footprint.** Group candidates by the files/dirs each one - touches — read the ticket and grep the code when unsure; don't infer the footprint - from the title. Present the clusters as a table. Within a cluster, pick **at most - one**; the rest go sequential. - -1. **Stale-premise pre-flight.** Flag any candidate whose fix may already be shipped - (checklist items matching recently-merged PRs; a "Done" tracker state that might be - a bot-driven auto-complete rather than actual code). Verify against the live code - **before** recommending it — premise-check-before-build is the house style. - -1. **Scope outward-safe.** A session that would push to an external system, send a - notification, or post to a customer-facing channel gets scoped to its **in-repo / - authoring half**; the gated outward step stays an operator action after merge. - -1. **Assign an effort tier.** The risk read you just did (cluster + stale-premise + - outward-safe) also sets **how much reasoning each lane gets** — tag each chosen - lane `low` / `medium` / `high` (→ `max` for the gnarliest) from the [lane-risk → - effort tier map](#per-lane-effort-tier-risk--reasoning-effort--model) below, so the - launch step can resource it. - -1. **Recommend + confirm.** Propose a disjoint batch (one ticket per cluster) plus the - residual shared-file watch-outs (e.g. two tickets that *might* both touch the same - schema file), then let the operator choose the set. Tag each proposed lane with - **both** its effort tier and its [merge class](#per-lane-merge-class-self-merge-vs-operator-merge) - (self-merge / operator-merge) up front — deciding the merge boundary at plan time, - not at merge time, stops a batch stalling on ad-hoc "can I merge this?" calls and - tells each lane whether it may self-merge or must hand back. - -1. **Launch each + relay a kickoff.** Run `scripts/dev_session.sh new ` per - chosen ticket (see below) and relay each copy-paste line **with a kickoff prompt** - the operator pastes as the session's first message: - - > Read tracker ticket `` (+ any recipe in `docs/handoff.md`). Pre-flight its - > premise against the live code before coding. Branch `dev/` is ready. - > **Suggested effort: ``** (``) — set your session's - > model (and reasoning effort, if your client exposes that control) accordingly - > before starting. Draft PR on first push → mark ready when done → `/pr-watch` to - > green. Heads-up: a parallel session owns `` — if you need to touch - > ``, flag it before committing. **Do not edit `docs/handoff.md` or - > `docs/friction-log.md`** — those are cockpit-owned; put your handoff (what - > shipped, lessons, deferrals) in the **PR body** so the joint wrap-up can - > aggregate it. - - Interactive `new` launches your agent CLI in the **operator's own shell**, so it - can only *suggest* the tier — the operator applies it (model, and reasoning effort - if exposed) when they start the session. An **unattended/headless** batch sets the - tier itself — see [Unattended / headless launch](#unattended--headless-launch--new---headless). - -After launch, **this** session is the cockpit: `list` (or `list --watch` for an -auto-refreshing board that flags each CI/commit/PR transition) is the live board, and -you sequence (merge → rebase) any two PRs that end up sharing a file. Two-collision -example worth stating to the operator up front: if two batched tickets both live under -one package but edit different files, they're safe — name the one file (often a -shared schema module) that would force them sequential if both need it. - -### Per-lane effort tier (risk → reasoning effort + model) - -A mechanical doc lane shouldn't burn max-effort on your most expensive model, and a -shared-primitive lane shouldn't run at low effort. Map each chosen lane to a tier from -its risk: - -| Lane risk | Typical work | Effort | Model | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------- | -------------------------------------------- | -| **Mechanical** | doc/comment-only edits, rename, copy tweak, config-value bump, dead-code delete, a `_latest`-rename sweep | `low` | inherited (`models.cheap` is fine) | -| **Standard** (default) | a normal feature/bugfix scoped to one package, test additions, a self-contained script | `medium` | inherited (`models.default`) | -| **High-stakes** | schema / data-shape change, a shared primitive (a state-sandbox library, a pipeline-state helper), a guard / gate / verifier, security, anything touching production cron/CI or shared `state/`, the merge-rules / scoring core | `high` (→ `max` for the gnarliest) | `models.expensive` | - -When unsure, round **up** — under-resourcing a risky lane costs a bad merge; -over-resourcing a cheap one costs only tokens. - -### Per-lane merge class (self-merge vs operator-merge) - -The same risk read that sets the effort tier also sets **who lands the PR** — -pre-classify it at plan time so a green lane isn't left waiting on an ad-hoc merge -decision (and so an autonomous batch knows what it may close itself): - -| Lane risk | Merge class | Who merges | -| ------------------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------ | -| **Mechanical** / **Standard** | **self-merge** *(autonomous/headless batches only)* | the lane (or cockpit), once green-and-clean with one independent review pass | -| **High-stakes** | **operator-merge** | always the operator — never self-merged, even when green | - -Operator-merge is the floor for anything in your project's high-risk classes: -data-shape / fetcher / config-semantics changes, shared primitives, a **guard / gate / -verifier / send-path / kill-path** (see -[`safety-critical-changes.md`](../rules/safety-critical-changes.md)), security, PII, -or anything touching production cron/CI or shared `state/`. Normal **interactive** -sessions leave *all* merges to the operator regardless of class — self-merge is an -autonomous-session behavior (see -[`docs/autonomous-session-playbook.md`](../../docs/autonomous-session-playbook.md)). -When unsure, classify **operator-merge**. - -**How the tier reaches the lane.** It depends on the launch mechanism: - -- **Headless lanes — fan out through a multi-agent workflow to get the *real* effort - dial (preferred for a multi-lane batch).** A workflow launcher passes each lane its - tier directly — one sub-agent per lane, each given its own `effort`/`model` — so - **both** halves of the tier (reasoning effort *and* model) actually take effect. - This is the only launch path on which the `effort` half is real; reach for it - whenever the lanes are tiered differently. Recipe + caveats in [Unattended / headless - launch](#unattended--headless-launch--new---headless). -- **Headless lanes — a single background sub-agent per lane is the *model-only* - fallback.** If your agent runtime's background-task tool exposes `model` but no - `effort` parameter, spawning lanes as individual sub-agents sets each lane's *model* - per the tier while the *effort* reaches it only as a prose hint in the prompt - ("tier: high, think carefully") — nominal, not a real setting. Fine when there's a - single lane, when every lane shares a tier, or when you want independently-stoppable - cockpit-side agent objects; use the workflow-fan-out path when per-lane effort - differentiation is the point. Either way the tier is **sourced from the plan's risk - assessment**, one per lane. -- **Interactive `new` lanes**: the kickoff only *suggests* the tier (above); the - operator sets their own session's effort/model. - -**Default-safe.** A lane with no assigned tier inherits the cockpit's current -effort/model — i.e. unspecified ⇒ today's behavior, no regression. The tier is an -*optimization* of a working default, never a prerequisite. - -### Joint wrap-up — the cockpit owns the handoff - -`docs/handoff.md` and `docs/friction-log.md` are shared *narrative* files: a -per-session edit to either collides at merge **and** pollutes a focused code PR, so -sessions never touch them (the kickoff says so). Each session's handoff rides its -**PR body** — the one channel that's committed, reviewed, and visible across -worktrees (dev-session `state/` sandboxes are isolated by design, so a session's -worktree scratch is invisible to the cockpit). When the batch is closed — every PR -merged or consciously parked — run the joint wrap-up **from this cockpit session**: - -1. **Reconcile every launched scope to a terminal state first — before reading any - narrative or writing any block.** An aggregate "all merged" is *not* evidence a - scope shipped: a silently-dead session (never started, 0 commits, no PR, branch - still at the base tip) can get closed as done if nothing checked per-branch. Run - the reconciler over the batch's launched scopes: - - ```bash - scripts/reconcile_sessions.sh - ``` - - For each `dev/` it resolves a **merged** PR (`gh pr list --head dev/ - --state merged`) or marks it **parked** with the reason (`EMPTY — 0 commits, never - started`, `PR closed unmerged`, `N commit(s), no PR opened`) or **open** (still in - flight), then prints the `launched N, merged M, parked K` tally — exit 3 if any - scope is open or parked, 0 only when all merged. **Do not write the wrap-up block - until every launched scope is merged or consciously parked.** A scope that - reconciles to **open** means the batch isn't closeable — finish or park it first. A - scope that reconciles to **parked** gets named as parked in the block, never folded - into "all shipped". (Pass the scopes explicitly — `rm` removes session dirs, so a - scope already torn down won't auto-discover; the cockpit knows the launched set.) - - The reconciler is **mechanism-agnostic** — it keys on branch / PR head ref, so it - also covers batches *not* launched via `/parallel` (a background sub-agent fan-out, - headless lanes). For those, pass branches directly or a glob instead of scopes: - - ```bash - scripts/reconcile_sessions.sh --match 'feat/login-*' # every local+remote branch matching the glob - ``` - - With no args it discovers in-flight lanes from **both** session dirs and live `git - worktree`s (deduped by branch), so a background-sub-agent worktree gets the same - `launched/merged/parked` net that catches a dead session. - -1. Read each **merged** PR's narrative: `gh pr view --json title,body` per merged - batch PR (parked scopes have no landed narrative to read). - -1. Write **one** "Latest session" block for the whole batch via `/wrap-up` — open - with the `launched N, merged M, parked K` line, then PRs landed, collisions - avoided, and each parked scope with its reason. Not one block per session. - -1. Open it as its own `chore: update handoff` PR (this checkout sits on the protected - branch, so the handoff edit goes through a branch + PR like everything else; - mirrors the existing `chore: update handoff` cadence). - -Overflow that doesn't fit a PR body (richer lessons, friction-log entries) goes to a -uniquely-named `docs/handoff/.md` fragment that rides the session's **own** PR -— disjoint path, zero collision — and the cockpit folds it into the wrap-up block and -deletes it. Start with PR-body-primary; only reach for fragments when bodies prove too -thin. - -## Starting a new session - -A new session must be launched from the operator's **own shell** (a slash command -can't `cd` the operator into a new worktree and open a fresh agent session there). So -when asked to start one, run: - -```bash -scripts/dev_session.sh new -``` - -substituting a lowercase slug for `` (e.g. `feat-graduation-flow`). The script -prints a copy-paste line — `cd && export DEVKIT_STATE_ROOT=… && export -DEVKIT_ROOT=… && `. **Relay that line to the operator** and tell them -to run it in a new terminal; don't try to start the session yourself. Options: -`--base ` (default `main`), `--prefix

` (default `dev` — parallel-session -branches get their own namespace to avoid colliding with hand-named feature branches), -`--branch ` to override the whole name. - -### Unattended / headless launch — `new --headless` - -Interactive `new` is operator-launched by design (it prints a copy-paste line and the -rule above says *don't start the session yourself*). That's the wrong shape for an -**unattended** batch — a background sub-agent or a cloud session that should drive a -*sandboxed* lane without a human in the loop. `--headless` is for exactly that: - -```bash -scripts/dev_session.sh new --headless -``` - -It creates the worktree + sandbox exactly as `new` does, but instead of the human -block it: - -1. **Writes a sticky `/.devkit_state_root` marker** holding the absolute - sandbox path. This is the mechanism that makes a headless lane safe: a background - sub-agent's shell calls don't share a shell, so an exported `DEVKIT_STATE_ROOT` - doesn't survive call-to-call. Your state-sandbox resolver reads the marker - (walking up from cwd) when the env var is unset, so the lane's `state/` writes - isolate into the sandbox **automatically** — no env gymnastics in the prompt. - (Precedence: env var → marker → repo-root default. Cron/CI writes no marker, so - it's unaffected.) -1. **Prints a JSON descriptor to stdout** (diagnostics go to stderr, so stdout is - clean JSON): `{"scope","branch","worktree","state_root","repo_root","base", - "prompt_preamble","env"}`. `prompt_preamble` is the canonical lane-contract text - below — the launcher **MUST** prepend it verbatim to the lane's task prompt. `env` - (currently `{"DEVKIT_REFUSE_UNSANDBOXED_STATE": "1"}`) flips the unsandboxed-write - guard from *warn* to *refuse*, by default, for every headless lane — so a lane - whose marker resolution somehow fails (deleted marker, cwd escaped the worktree) - hard-errors on a `state/` write instead of silently landing in prod. Interactive - `new` and cron/CI never set either field. - -### The lane-contract preamble (inject this verbatim) - -Every mechanism that hands a task prompt to a headless lane — a multi-agent -workflow fan-out, a single-background-sub-agent fallback, or any future launcher — -**MUST prepend the same fixed contract text** ahead of the task-specific -instructions. This is the fix for an idle-stall failure mode: a rule that lives only -in a memory or in this doc's prose can't bind a freshly spawned lane, because a fresh -agent has no memory and doesn't read `parallel.md` unless told to. The contract must -be *in the prompt itself*, every time. - -Fetch the current text with `scripts/dev_session.sh print-contract` (plain text, no -JSON) or read it straight off the `prompt_preamble` field of any `new --headless` -descriptor — **do not hand-copy/paraphrase it into the launcher script**, since the -two would drift; always read it fresh from one of those two sources so a future edit -to the contract propagates without touching `parallel.md`. As of this writing it -reads: - -``` -LANE CONTRACT (binding): -- Actively poll your PR's CI at a bounded cadence (e.g. `gh pr checks ` every few minutes, capped around 30 min) until it is fully green. Never stop to idly wait on a "monitor", a timer, or someone else's watcher — you are the one polling. -- Your run ends ONLY at the terminal state. Never stop early to wait for a watcher, monitor, or timer of any kind — if you need to wait on anything, poll it yourself with a bounded until-loop. -- Stop at draft-PR-green and hand off. Open your PR as a DRAFT on first push and leave it in draft. Do not mark it ready, do not merge — the cockpit owns ready-for-review, the review pass, and the terminal merge. -- Report every finding, decision, and open question in your FINAL TEXT response — that is the only channel back to the cockpit. Never rely on a peer message to another agent to deliver results. -- If you spawn sub-agents of your own, hold them to the same rule: they return findings in their final text, never via peer message — peer messaging by type-name is unreachable from a spawned sub-agent. -- Never edit docs/handoff.md or docs/friction-log.md — those are cockpit-owned shared narrative files. Put your handoff (what shipped, lessons, deferrals) in the PR body instead. -- Run `git branch --show-current` before every commit to confirm you are on your own lane branch, never the protected branch. -``` - -**Launch contract (cockpit usage).** Both launchers drive the same `new --headless` -worktrees; the choice is whether the per-lane *effort* dial needs to be real. **Both -MUST prepend the lane-contract preamble above to every lane prompt** — it is not -optional for either path. - -**Preferred for a multi-lane batch — a workflow launcher (the real effort dial).** Run -`new --headless ` once per chosen scope, collect each one's JSON descriptor -into a list (attaching the per-lane `effort`/`model` tier from the plan's risk read), -then drive the lanes from a *single* fan-out that gives each sub-agent its own -`{effort, model}` — the one path on which the tier's `effort` half actually takes -effect. Pseudocode: - -```js -// args.lanes = [{scope, worktree, branch, ticket, effort, model, prompt_preamble}, …] -// — one per `new --headless` descriptor (prompt_preamble copied straight off it). -// effort ∈ low|medium|high|max (omit ⇒ inherit cockpit effort); model ∈ models.cheap|default|expensive (omit ⇒ inherit). -runInParallel(args.lanes.map(lane => () => - spawnAgent( - `${lane.prompt_preamble}\n\n` + - `Work in worktree ${lane.worktree} on branch ${lane.branch} (cd there first — its state sandbox is active via the on-disk marker, so your state/ writes isolate automatically). ` + - `Read tracker ticket ${lane.ticket}, pre-flight its premise against the live code, implement, draft PR on first push, drive it to green-and-clean, then hand off per the contract above.`, - { label: lane.scope, effort: lane.effort, model: lane.model } // omit effort/model to inherit (default-safe) - ) -)) -``` - -Four things to keep right: **(1)** `lane.prompt_preamble` is prepended verbatim, -ahead of everything else, on every lane — never abbreviated to "follow the usual -contract" (that's exactly the prose-reference that failed to bind a lane before). -**(2)** do **not** open a second worktree on top of `--headless` — it already owns the -worktree+sandbox, so a second one would have no marker and lose isolation. **(3)** A -lane with no assigned tier omits `effort`/`model` and inherits the cockpit's — the -same default-safe fallback as everywhere else. **(4)** Check what compute budget your -fan-out mechanism draws from before running a large batch, and monitor it via -whatever live-progress view your runtime exposes, plus `list --watch` on the lanes' -branches/PRs. - -Note on the `env` field: if your fan-out or background-agent tool doesn't expose a -parameter to inject env vars into the spawned lane's process, `DEVKIT_REFUSE_ -UNSANDBOXED_STATE` cannot be force-set through that launch path the way -`prompt_preamble` can be force-injected into the prompt text. In the normal case this -is moot: the marker already resolves the sandbox, so the guard never fires. The `env` -field exists for launchers that DO control the spawned process's environment (a -custom subprocess-based batch driver) — read it and export it there. Treat this as a -documented, conservative gap, not a silent one. - -**Fallback — a single background sub-agent per lane (model-only).** Parse the -descriptor and spawn a background sub-agent whose prompt is **the `prompt_preamble` -field, prepended verbatim**, followed by the task-specific instructions naming the -`worktree` path — e.g. *"`` Work in worktree `` on branch -``. The state sandbox is active via its on-disk marker — your `state/` writes -isolate automatically. Read tracker ticket ``, pre-flight its premise, draft PR on -first push, drive it to green-and-clean, then hand off per the contract above."* Same -no-second-worktree rule and same prepend-verbatim requirement as the workflow path. -This path sets each lane's `model` per the tier but **not its effort** if your -runtime's background-task tool has no effort dial — fine for a single lane, lanes that -all share a tier, or when you want individually-stoppable cockpit-side agent objects. -The how-the-tier-reaches-the-lane mechanics + the default-safe fallback live in -[Per-lane effort tier](#per-lane-effort-tier-risk--reasoning-effort--model). - -**When to use which.** Attended work (operator at a terminal) → plain `new`. -Unattended pipeline-touching work (any lane that writes `state/cache/`) → `new ---headless` so the sandbox is active without a surviving shell export. This is the -`/parallel` vs bare-background-agent decision rule: *does the lane write `state/`? → -it needs a sandbox → `new --headless`, not a bare background worktree.* This should be -**guarded, not just documented**: your state-sandbox write path should warn when an -unsandboxed lane (no `DEVKIT_STATE_ROOT`, no marker, job-name env unset, in a linked -worktree) writes repo-root `state/` — and `new --headless` sets -`DEVKIT_REFUSE_UNSANDBOXED_STATE=1` by default (the `env` descriptor field + activate -snippet above) to make that a hard error rather than a warning. Cron/CI and normal- -interactive paths are unaffected. - -## Finishing a session - -After its PR has merged: - -```bash -scripts/dev_session.sh rm -``` - -Removes the worktree + sandbox; deletes the branch only if it's merged into the -protected branch (kept with a warning otherwise). Refuses if the worktree has -uncommitted changes unless `--force`. `scripts/dev_session.sh path ` prints the -worktree path (handy for `cd "$(scripts/dev_session.sh path )"`). - -## Notes - -- **Why the sandbox.** Writes from a session (any skill/script that writes `state/`) - land in that session's sandbox; shared-cache *reads* take the newer of - sandbox-vs-main so a session still sees fresh prod caches read-only. This is what - lets two sessions run data skills concurrently without corrupting `state/cache/`. - See Principle #3 in `PRINCIPLES.md`. -- **Pairs with** `/session-start` (orient within one session) and your project's - branching convention (draft PR → ready → watch-and-fix). `/parallel` is the - *across-sessions* view; `/session-start` is the *within-session* one. -- Read-only by default (`list`; `plan` is read-only until the operator confirms a - batch). `new`/`rm` mutate worktrees only — never the repo's own tree, never prod - `state/`. +Treat `$ARGUMENTS` as the requested parallel-development action and arguments. +Resolve the engine path from the repository root. diff --git a/.claude/commands/pr-watch.md b/.claude/commands/pr-watch.md index 3df70f5..c732d5a 100644 --- a/.claude/commands/pr-watch.md +++ b/.claude/commands/pr-watch.md @@ -1,87 +1,4 @@ -Watch a pull request until it is **green and clean** — CI fully passing and every -review-bot / human review finding fixed or replied-to — then report. This is the loop -your project's "PR follow-through" policy mandates after opening or pushing to a PR; -run it without being asked. +Read `docs/agentic-dev-kit/workflows/pr-watch.md` completely and follow it. -**Input:** an optional PR number. With none, the current branch's open PR is used. - -Engine: `scripts/pr_watch.py` (deterministic — check rollup + comment union across -issue/review/inline surfaces, noise-filtered, diffed against a per-PR seen-set). You -drive the loop + apply the judgment. - -## Loop - -Repeat until the report says **done**: - -1. **Poll.** `uv run scripts/pr_watch.py --json` (omit `` for the current - branch). Read `done`, `checks` (`all_green`, `failing[]`, `pending`), and - `new_comments[]`. - -1. **If `done` (checks all green + nothing new):** stop the loop and report — PR #, - the green check count, and "no outstanding review findings." You're finished. - -1. **If checks are still `pending` and there are no new comments:** nothing to do yet - — wait and re-poll (see Pacing). CI can take 20–30 min; that's expected, keep - going. - -1. **If a check is `failing`:** investigate (`gh run view --log-failed`, or - `gh pr checks `), fix the cause in the code, run your project's local gate - (e.g. `make check`), commit, and `git push`. The push re-triggers CI — keep - looping. - -1. **If there are `new_comments`:** handle each with judgment — - - - **Real finding** (a bug, a missing guard, a correctness/clarity issue): fix it in - the code, commit, push. Re-running the local gate first. - - **Nitpick you disagree with** (style preference, out-of-scope, already-correct): - **reply with a brief reason** rather than changing code — `gh pr comment - --body "..."` for a top-level reply, summarizing what you addressed vs. skipped - and why. - - Verify each finding against the *current* code before acting — some go stale - across rounds (a later commit already fixed it). - -1. **Acknowledge the round:** once you've handled this round's findings, run `uv run - scripts/pr_watch.py --mark-seen` so they don't resurface. `--mark-seen` never - re-polls `gh` — it promotes the exact set of comment keys that your last `--json` - poll reported (persisted locally in the per-PR state file as a "pending" set) into - the seen-set, then clears it. This makes the ack deterministic: a comment that - lands on the PR *after* your read-poll and *before* `--mark-seen` was never part of - that pending set, so it can't be acked by this call — it stays unseen and surfaces - on your next poll instead of being silently buried. Calling `--mark-seen` without a - prior poll (nothing pending) acks nothing and says so (`note` in the output) — - always poll-and-read first. - -1. **Pace the next poll** (see below), then go to step 1. - -## Pacing - -Self-pace on a bounded cadence — don't busy-wait: - -- **Review bots** land their first pass ~2–5 min after a push. Poll ~every 180–270 s - while waiting on them (stays inside the prompt-cache window). -- **CI** can run 20–30 min. While only checks are pending, a longer 300–600 s cadence - is fine. -- After you push a fix, expect a fresh CI run + possibly a re-review — keep looping; - don't declare done off a stale poll. - -## Stop conditions - -- **Done** — `done: true` (green + clean). Report and finish. This is the goal. -- **Stuck / needs a decision** — a check fails for a reason you can't resolve (a - flaky-infra failure that won't clear on re-run; an external dependency; a finding - that needs an operator product/design call). Stop, report the specific blocker, and - ask. Don't loop forever on something only the operator can unblock. -- **Bound the loop** — if you've gone ~8–10 rounds without converging, stop and - summarize where it stands rather than looping indefinitely. - -## Notes - -- The seen-set lives at `state/pr-watch/.json` (gitignored). It's per-PR, so - re-running on a different PR starts fresh. -- Known auto-noise from your review bots (billing/usage notices, walkthrough / "no - actionable comments" summaries) is filtered out by the engine — edit the - noise-marker list in `scripts/pr_watch.py` for your own bot mix; those never count - as findings. -- This is interactive-only. A scheduled job that opens its own PRs should be excluded - from this loop by your cron/CI runner's job-name signal, so an automated open never - silently enters an unattended watch loop. +Treat `$ARGUMENTS` as the optional PR number or additional watch context. Resolve +the engine path from the repository root. diff --git a/.claude/commands/session-start.md b/.claude/commands/session-start.md index 06b96c2..faebf35 100644 --- a/.claude/commands/session-start.md +++ b/.claude/commands/session-start.md @@ -1,174 +1,4 @@ -Start-of-session briefing — the bookend to `/wrap-up`. Reads the living handoff, the -friction-log inbox, your tracker, and live repo/CI state, then proposes **what to do -next**: candidates grouped by **urgency** and tagged `[size · model · mode]`, ending -with one recommendation. +Read `docs/agentic-dev-kit/workflows/session-start.md` completely and follow it. -## What it reads - -| Source | How | -| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| Living handoff | `docs/handoff.md` (`config/dev-model.yaml → paths.handoff`) — latest session block + every "Next:" / "Follow-ups:" trail | -| Friction inbox | `docs/friction-log.md` (`paths.friction_log`) — entries since the last "Backlog migrated" marker | -| Tracker backlog | your tracker's list-issues command/script — project `tracker.project_name` (open only — drop `completed`/`canceled`) | -| Open PRs | `gh pr list` — anything draft / CI-red / awaiting-review (the PR-follow-through rule) | -| Working tree | `git status --short` + `git branch --show-current` — unfinished business from last session | -| CI/cron health | your cron/CI runner's status command (adapt to your infra — e.g. a wrapper script that logs recent job outcomes) | -| Config drift | your host config-apply step, if you have one (e.g. a `verify --json`-style check comparing committed config against applied host state) — drop this bullet entirely if it doesn't generalize to your setup | - -### 0 · Gather (run in parallel) - -Fire these together — they're independent: - -- `git status --short` and `git branch --show-current` -- `gh pr list --state open --json number,title,isDraft,reviewDecision,statusCheckRollup,author --limit 20` (`author` distinguishes a **cron/automation-opened** PR from one a person opened; those are guarded out of `/pr-watch` by your cron runner's job-name signal, so their bot findings get no automated follow-through and the next cockpit must adopt them — see Step 2) -- your cron/CI health command (adapt to your infra) -- your config-drift check, if you have one (parse its output for a 🔴-worthy line in Step 2) -- Read `docs/handoff.md` (focus: the **"Latest session"** block and its `Next:` / `Follow-ups:` lines, plus the top-of-file "Last updated" trail for the active sprint) -- Read `docs/friction-log.md` (the inbox — entries above the most-recent `## … — Backlog migrated to ` marker; everything below it is already ticketed) -- **Tracker** (optional — if the script/key fails, note the gap and continue): a field-limited list-issues call against `tracker.project_name` (id/identifier/title/url/state/priority/updated — avoid pulling full descriptions, which is what makes a naive "dump everything" call routinely overflow a tool's token limit). Discard issues whose state type is `completed`/`canceled`, and print a compact table sorted urgent(1) → low(4) with no-priority(0) last. A missing/invalid config or missing tracker credential should exit non-zero with a clear message — treat any non-zero exit as the optional-tracker gap (note it and continue) — never act on a partial payload. - -### 1 · Classify each candidate - -Turn the raw signals into a deduped candidate list. Each candidate gets an **urgency -bucket**, a **size**, a **model tier**, an **execution mode**, and a **source -pointer**. - -**Urgency** (the grouping axis): - -- 🔴 **Now** — broken or actively in-flight. A CI/cron failure on an active job that - needs recovery; an open PR that's CI-red or has unaddressed review (opening/pushing - a PR isn't done — watch-and-fix is the same task) — **including a cron/automation- - opened PR** (identified by `author` in the gather) with a changes-requested decision - or unresolved bot findings: your cron runner's job-name guard means `/pr-watch` - never watched it, so adopting it (run `/pr-watch `) is this session's job; - uncommitted work from last session that should be finished or committed; the - handoff's explicit current `Next:` **iff** it's the active sprint's blocking step; - any entry your config-drift check flags — a merged config change that's inert on - the host until applied is an **operator host-action** reminder, not a delegatable - build candidate — render it without model/mode tags (see Step 2). -- 🟡 **Soon** — this week's clear next steps. Active-sprint follow-ups, time-bound - items ("validate Wednesday's run"), medium-severity friction-log entries, - started/high-priority tracker tickets. -- 🟢 **Whenever** — backlog. Low-severity friction-log entries, lower-priority tracker - tickets, nice-to-haves. - -**Size** `S / M / L` — scope/effort: `S` ≤ one small PR (~30 min); `M` a focused -single-concern PR (~an hour); `L` multi-PR or a sprint slice. - -**Model tier** — the *intelligence* the work needs (orthogonal to size — a large -mechanical sweep is `L · cheap`; a one-line calibration decision is `S · expensive`). -The three tier names below (`models.cheap` / `models.default` / `models.expensive` in -`config/dev-model.yaml`) are **illustrative** — map them to whatever model names your -agent runtime exposes. **Default to the middle tier**: on well-specified, -self-contained work — even work that must be grounded against live code/schema — it -matches top-tier output at meaningfully lower cost, and it draws from the larger of -your two paid quota buckets if your plan has one. Reserve the top tier for the -genuinely tough problems; keep the cheap tier for the purely mechanical: - -- **`models.cheap`** — purely mechanical / deterministic: renames, dead-code removal, - config sweeps, applying a known one-line fix. No grounding or judgment needed. -- **`models.default`** — **the default tier.** Self-contained build / refactor / doc - work with clear acceptance criteria, *including* work that must verify itself - against live code/schema; given a precise spec it produces top-tier-grade output. - This is the default `delegate` tier. -- **`models.expensive`** — reserve for the *really tough* tasks: design decisions, - calibration / threshold choices, ambiguous or emergent scoping, security-sensitive - changes, cross-system reasoning — anything where being wrong is expensive and hard - to catch in review. These almost always run `inline` (expensive tier + judgment ⇒ - inline), not delegated. - -**Execution mode** `inline / delegate` — *where* the work runs once you greenlight it; -the token lever (a plan, not an action — nothing launches until the operator picks one -in Step 3): - -- **`delegate`** — spawn the item as a sub-agent (defaulting to `models.default`) while - this cockpit session stays on the expensive tier for orchestration + review. The - default for **self-contained, clearly-specified work** (no mid-flight judgment — - even when it must verify against live code): the cheaper tier does the building, the - expensive model never burns tokens typing it out, and the cockpit still reviews the - result and drives the PR. Drop to a cheap-tier delegate only for purely mechanical - sweeps; an expensive-tier delegate is rare — work hard enough to need it usually - wants `inline` so you can steer it. -- **`inline`** — do it in this session. Right when the item is **expensive-tier** - (high judgment), needs **live iteration / your input as it unfolds**, or is - **exploratory** (scope emerges as you go). If you want a cheaper tier for an inline - item, switch this session's model for that stretch — better than delegating when you - need the back-and-forth. - -Rule of thumb: `self-contained + clear spec ⇒ delegate to models.default` (cheap-tier -only for purely mechanical sweeps); `really tough / high-judgment, or -interactive/exploratory ⇒ inline on models.expensive`. When in doubt, default `inline` -(no regression vs today). - -**Source pointer** — every item shows where it came from so you can drill in: -`handoff`, `friction-log `, a tracker ticket id, `PR #NNN`, or the job name. - -**Rules:** - -- **Dedup.** An item that appears in more than one source (a friction-log entry that's - also a tracker ticket, say) is listed **once**, with the most authoritative pointer - (tracker > handoff > inbox) and any others noted inline. -- **Don't invent work.** Only surface candidates traceable to one of the sources - above. If a source is empty or clean, say so — a quiet bucket is a real result. -- Keep each line to one sentence of *what* + the tags + the pointer. No essays. -- **Composing an autonomous/overnight self-merge batch:** also tag each lane with its - predicted merge class (self-merge / operator-merge — see `/parallel`'s per-lane - merge-class table) and report the split to the operator before launch — e.g. "2 will - self-merge, 3 held for you." - -### 2 · Render the briefing - -```text -🧭 Session Start — - -Where things stand - • () · open PRs · CI/cron: - • Active sprint: - • Last session: - -What to do next - -🔴 Now - • [ · · ] - • N config change(s) INERT pending a host apply step — , , … -🟡 Soon - • [ · · ] -🟢 Whenever - • [ · · ] -``` - -- Omit a bucket entirely if it's empty (don't print "🔴 Now: nothing"), but if **all** - of Now+Soon are empty, say so plainly — e.g. `✅ All clear — nothing urgent or due - this week; see 🟢 Whenever for backlog.` -- Order items within a bucket by leverage (blocking > high-value > cheap-win). -- The config-drift line only appears when your drift check reports something - outstanding; name the affected items. A less-urgent "orphan" class of drift (config - present with nothing applying it, or vice versa) is a separate, lower-urgency - concern — mention it only under 🟢 Whenever if present, never conflated with the 🔴 - line above. - -### 3 · Recommend one, then wait - -End with a single pick and a one-line why, then **stop** — let the operator choose. Do -not auto-start the work. - -```text -👉 My pick: [ · · ] — - sub-agent and review the result here, keeping this session on the expensive tier." | inline ⇒ "We'll run it in this session — say the word if you want me to switch models for it."> - Want me to start it, or pick another? -``` - -Rationale heuristics: prefer 🔴 Now if the bucket is non-empty; otherwise the active -sprint's blocking next step; break ties toward the highest value-per-effort (small + -high-leverage). - -## Notes - -- **Read-only.** This skill never edits, commits, or starts work — it only reports - and recommends. It's safe to run anytime to re-orient mid-session. -- If `docs/handoff.md` or `docs/friction-log.md` is over its line budget (a - session-start tripwire may have warned), mention it as a 🟢 housekeeping item - (`/wrap-up` sweeps the handoff; `/triage-friction-log` graduates the inbox) — don't - sweep inline. -- Pairs with `/wrap-up` (session end). Use `/pr-watch` to action a 🔴 PR item, - `/triage-friction-log` to clear the inbox. +Treat `$ARGUMENTS` as additional session context. Resolve all configured paths from +the repository root and `config/dev-model.yaml`. diff --git a/.claude/commands/wrap-up.md b/.claude/commands/wrap-up.md index f32975b..0804091 100644 --- a/.claude/commands/wrap-up.md +++ b/.claude/commands/wrap-up.md @@ -1,77 +1,4 @@ -End-of-session wrap-up. Update the living handoff and commit. +Read `docs/agentic-dev-kit/workflows/wrap-up.md` completely and follow it. -## Steps - -1. **Read the current handoff** — `docs/handoff.md` (`config/dev-model.yaml → paths.handoff`) - -1. **Review what changed this session** — check `git diff` and `git log` since the - handoff's "Last updated" date - -1. **Update `docs/handoff.md`**: - - - Move completed work from "In Progress" to "Done" (with a one-line summary of - what shipped) - - Add any new items discovered during the session to the appropriate section - - Update sprint status if a sprint boundary was crossed - - Remove resolved housekeeping items - - Update the "Last updated" date to today - - Keep it concise — the handoff is a handoff document, not a changelog - -1. **Capture friction** — if this session surfaced any bug, friction, or idea specific - to a workflow (a skill, a cron/CI job, a pipeline), append a short entry to - [`docs/friction-log.md`](../../docs/friction-log.md) under a dated `## YYYY-MM-DD` - heading: the observed issue, a severity (**H**/**M**/**L**), and a proposed fix. - This is the documented session-end practice — it captures learnings while fresh; - they later graduate to your tracker via `/triage-friction-log`. Add to the inbox - only — don't graduate or sweep here. Skip if nothing workflow-specific came up. - -1. **Suggest a next-session starter** — if the session ends with a *clear* follow-up, - hand the next session a running start: - - - **One obvious next thing** → add it as a final `▶ Next: ` line at the end - of the latest session block in `docs/handoff.md`, and print the same starter in - the chat. Make it concrete and copy-pasteable: a `/skill args` invocation, or a - one-line task prompt that names the file / ticket / PR (e.g. `▶ Next: /pr-watch - 1131 — fix review findings then self-merge`). The `▶ Next:` line is an allowed - addition (like the archive sweep), not a structure change to ask about. - - **Diffuse / several threads** → don't invent a false single thread; tell the - operator to open next session with `/session-start` (it re-reads handoff + - inbox + tracker + live repo/CI state and re-proposes what to do). - - **No clear follow-up** → skip this step. - -1. **Update any project-status doc** (e.g. a dashboard snapshot) if any metrics - changed this session — adapt this step to whatever presentation artifact your - project keeps; skip if you don't have one. - -1. **Keep the handoff docs lean.** After adding this session's block, run - `uv run scripts/check_doc_budget.py`. If it warns that `docs/handoff.md` is over - budget, run `uv run scripts/archive_plan_sessions.py` — it deterministically keeps - the newest ~6 session blocks live, moves the rest into `docs/handoff-history.md`, - and trims the megaline. Stage **both** files (`docs/handoff.md` + - `docs/handoff-history.md`) into this commit. If `docs/friction-log.md` is over - budget, don't sweep it inline — note it and recommend `/triage-friction-log` - (graduating the inbox needs tracker writes + operator approval). This is what - stops the handoff docs from ballooning between archive sweeps. - -1. **Commit + PR the handoff update — never commit to your protected branch - directly.** Commit as `chore: update handoff — [one-line summary of session work]` - (stage `docs/friction-log.md` too if you added an inbox entry this session, and - `docs/handoff-history.md` if the archive sweep ran). If you're already on the - session's feature branch, this is just another commit on that branch's PR. If - you're on the protected branch (e.g. a planning-only session), branch first - (`chore/update-handoff-`) before committing, then push and open a PR. Either - way, once there's nothing left to push, **mark the PR ready** so it gets reviewed, - and run the watch-and-fix loop (`/pr-watch`) to merge — per your project's - branching convention. - -## Rules - -- Do NOT add session-specific detail (decisions, debugging steps, conversation - context) — that belongs in session-scoped scratch notes or memory, not the living - handoff -- Do NOT change the handoff's structure or add new sections without asking — but the - `archive_plan_sessions.py` sweep (moving old session blocks to `handoff-history.md`) - and a single `▶ Next:` starter line at the end of the latest session block are both - documented additions, not structure changes, so do them without asking -- If a backlog item was promoted to a sprint epic, move it (don't duplicate) -- If the session produced no handoff-relevant changes, say so and skip the commit +Treat `$ARGUMENTS` as additional wrap-up context. Resolve all configured paths from +the repository root and `config/dev-model.yaml`. diff --git a/.claude/rules/safety-critical-changes.md b/.claude/rules/safety-critical-changes.md index 372a135..7e90a78 100644 --- a/.claude/rules/safety-critical-changes.md +++ b/.claude/rules/safety-critical-changes.md @@ -1,43 +1,8 @@ --- -paths: ["scripts/dev_session.sh"] -# Add your own send-path / gate / kill-path files or globs here — e.g. a release -# pipeline's approval-gate script, a destructive-operation guard, a signal/retry -# handler. This rule is only useful once its `paths:` glob actually matches the files -# in your repo that gate customer-facing sends or destructive/recovery operations. +paths: ["scripts/dev_session.sh", "scripts/devkit/dev_session.sh"] +# Add your own send-path / gate / kill-path files or globs here. This rule is useful +# only after its paths match the files that gate sends or destructive/recovery work. --- -# Safety-critical decision logic — review doctrine - -These files gate customer-facing sends, destructive operations, or process -kill/recovery paths. Four rules apply to any behavioral change here — each one earned -by a real shipped failure that CI-green + full unit tests did not catch (an -approval-matcher inversion; a send-gate with holes found only in review; a destructive -operation whose "safety" fix reintroduced the hazard; a kill-path that passed unit -tests but was broken in integration). See Principle #6 in `PRINCIPLES.md` for the -full doctrine this rule operationalizes. - -1. **Deterministic gate > NLP/keyword matcher.** A matcher over free-text (approval - keywords, cancel phrases) is inherently leaky — repeated review rounds on a - leaky matcher each tend to find a *new* wrong-send, not close the class of bug. - When the decision matters, the durable design is a deterministic artifact (a - stamp, a state field, an explicit flag) written at decision time and verified at - act time. Treat "we tightened the matcher" as a stopgap, not a fix. - -1. **Dual-lens review for customer-facing gates.** One review pass — however strong — - is not enough: an adversarial/bypass-focused pass and a general-correctness pass - routinely find **disjoint** holes. A send/publish gate needs BOTH lenses before - merge. A single-lens "converged" verdict is an incomplete review, not a green - light. - -1. **Adversarial review to convergence, not one pass.** Re-review after every fix - round until a full pass finds nothing new. Fix rounds on gate logic routinely - introduce their own regressions — treat "the last round found nothing" as - provisional, not proof of safety. - -1. **Kill/recovery paths need an integration test.** Unit tests on the handler are - insufficient — a kill-path can pass unit tests while the wrapper-level behavior is - broken. Exercise the real signal/timeout/retry path (or a faithful harness of it) - before marking the change done. - -Merge class: changes governed by this rule are **operator-merge** — never self-merge -them from an autonomous or lane session, even when green and clean. +Read `docs/agentic-dev-kit/safety-critical-changes.md` completely and apply that +doctrine to every behavioral change in the matched files. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c90ac2b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,20 @@ +name: Test + +on: + push: + pull_request: + +jobs: + toolkit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install test dependencies + run: python -m pip install pytest pyyaml + - name: Check shell syntax + run: bash -n init.sh scripts/dev_session.sh scripts/reconcile_sessions.sh scripts/lib/repo_root.sh scripts/hooks/pre-push + - name: Run state and portability tests + run: python -m pytest scripts/lib/state_paths/tests scripts/tests -q diff --git a/README.md b/README.md index 980824a..d21b744 100644 --- a/README.md +++ b/README.md @@ -55,25 +55,25 @@ back into the next session's briefing. ```mermaid flowchart TD - A([session start]) --> B["/session-start
reads handoff + friction-log
+ tracker + open PRs + CI"] + A([session start]) --> B["session-start
reads handoff + friction-log
+ tracker + open PRs + CI"] B --> C{"pick next work
by urgency"} - C -->|self-contained| D["/parallel
isolated worktree lanes
· cheaper model tier"] + C -->|self-contained| D["parallel
isolated worktree lanes
· cheaper model tier"] C -->|judgment / interactive| E["cockpit
work inline"] D --> F["open PR"] E --> F - F --> G["/pr-watch
poll · fix · reply
until green and clean"] + F --> G["pr-watch
poll · fix · reply
until green and clean"] G --> H{"risky change?
send-gate · destructive · kill-path"} H -->|yes| I["safety-critical review
deterministic gate · dual-lens
· operator sign-off"] H -->|no| J["merge"] I --> J - J --> K["/wrap-up
update handoff + log friction"] + J --> K["wrap-up
update handoff + log friction"] K --> L([session end]) K -. friction accrues .-> M[(friction-log)] M -. weekly .-> N["/triage-friction-log
single incident → tracker"] M -. weekly .-> O["/post-merge-systemize
2+ occurrences → a rule"] N -. tickets .-> P[(tracker + handoff)] - O -. new rule .-> Q[(CLAUDE.md rules)] + O -. new rule .-> Q[(agent rules)] P -. seeds next session .-> B Q -. binds next session .-> B ``` @@ -91,17 +91,32 @@ cp -r /path/to/agentic-dev-kit/. . ./init.sh # Answer the prompts (or accept the shown defaults), then: # -> open config/dev-model.yaml and fill in anything you skipped -# -> start your agent session and run /session-start +# -> start your agent session and invoke session-start ``` Ten minutes, start to finish. For a full worked example of a first session — from -adoption through `/wrap-up` — see **[`docs/getting-started.md`](docs/getting-started.md)**. +adoption through `wrap-up` — see **[`docs/getting-started.md`](docs/getting-started.md)**. + +### Agent runtime adapters + +The workflow definitions under `docs/agentic-dev-kit/workflows/` are shared. The +runtime adapters are intentionally thin: + +| Runtime | Repository adapter | Invocation | +|---|---|---| +| Claude Code | `.claude/commands/.md` | `/session-start`, `/wrap-up`, `/pr-watch`, `/parallel` | +| Codex | `.agents/skills//SKILL.md` | `$session-start`, `$wrap-up`, `$pr-watch`, `$parallel` | + +Set `runtime.default` in `config/dev-model.yaml`. The lane launcher reads its command +from `runtime.launchers`; shared workflows use the runtime-neutral +`cheap`/`default`/`expensive` tiers and translate them through +`models.runtime_mappings` only when the runtime exposes that control. ## Adopting into an existing repo The quickstart above assumes a fresh or near-empty repo. Dropping the kit into a -**mature** project — one that already has a `.claude/`, its own `config/`, a plan -doc, and CI — needs a lighter touch: a blind `cp -r` would clobber files. Adopt +**mature** project — one that already has agent configuration, its own `config/`, a +plan doc, and CI — needs a lighter touch: a blind `cp -r` would clobber files. Adopt selectively instead. **The [`/adopt`](.claude/commands/adopt.md) skill automates this.** Copy @@ -116,12 +131,13 @@ the adoption surfaced. The principles it applies: - **Point the config at what's already there.** Already have a `ROADMAP.md` or similar plan? Set `paths.handoff` to it in `config/dev-model.yaml` rather than adding a second plan file — or rename it to `handoff.md` if you prefer the kit's name. -- **Don't overwrite existing skills.** If `.claude/commands/.md` already exists - (a `wrap-up` of your own, say), keep it — copy in only the skills you don't have. +- **Don't overwrite existing skills.** Check both `.claude/commands/.md` and + `.agents/skills//SKILL.md`. Keep an adopter's existing workflow and install + only the missing adapters. - **Namespace the scripts if `scripts/` is organized.** If the repo keeps `scripts/` - in subdirs, vendor the kit's under `scripts/devkit/` (or similar) and update the - `scripts/…` references in the skills to match. The engine scripts discover the repo - root by walking up for `.git`, so they work at any depth. + in subdirs, vendor the kit under `scripts/devkit/` (or similar) and set + `paths.engines` accordingly. Every engine discovers the repo root by walking up for + `.git`, so it works at any depth without prompt rewrites. - **Check your CI/lint scope.** The `state_paths` tests use bare `assert` (they're pytest tests) — make sure a repo-wide lint scopes away from the kit's dir or ignores `S101` there. @@ -139,13 +155,15 @@ Each piece maps to one or more of the ten principles in | `docs/handoff.md` + `docs/handoff-history.md` | #1 Living-plan handoff | The one canonical plan — read at session start, updated at session end. Older sessions sweep to the history file once it crosses a line budget. | | `docs/friction-log.md` + `docs/friction-log-archive.md` | #2 Friction flywheel | Append-only inbox for bugs and rough edges, triaged on a cadence: single incidents route down to your tracker, real patterns graduate up into a rule. | | `scripts/lib/state_paths/` | #3 Cockpit + isolated lanes | The sandboxed state-path resolver so parallel agent lanes never clobber each other's scratch state. | -| `.claude/commands/*.md` (skills) | #1, #2, #3, #5 | `session-start`, `wrap-up`, `parallel`, `pr-watch`, `triage-friction-log`, `post-merge-systemize` — the operational surface that reads and writes the narrative files and runs the review loop. Plus `adopt` — the bootstrap for selective adoption into an existing repo. | +| `docs/agentic-dev-kit/workflows/` | #1, #2, #3, #5 | Runtime-neutral definitions for `session-start`, `wrap-up`, `parallel`, and `pr-watch`. | +| `.claude/commands/` + `.agents/skills/` | #1, #2, #3, #5 | Thin Claude and Codex adapters over the shared workflows. Claude also ships the project-specific `triage-friction-log`, `post-merge-systemize`, and `adopt` commands. | +| `docs/AGENTS-sections.md` | #4, #5, #6 | Ready-to-merge persistent instructions for Codex adopters. | | `docs/CLAUDE-sections.md` | #4 Merge classes, #5 PR follow-through | Ready-to-paste CLAUDE.md sections: risk-based PR splitting, the mandatory watch-to-green loop, execution rules, the rules-layout convention. | | `docs/autonomous-session-playbook.md` | #4, #5, #7 | The full operating contract for operator-requested autonomous sessions — branch hygiene, sequencing, local gate, draft→ready, watch-and-fix to merge, self-merge policy. | -| `.claude/rules/safety-critical-changes.md` | #6 Safety-critical doctrine | The review doctrine for send-gates, destructive operations, and kill/recovery paths — deterministic gate over matcher, multi-lens review, human sign-off only. | +| `docs/agentic-dev-kit/safety-critical-changes.md` | #6 Safety-critical doctrine | Shared doctrine for send-gates, destructive operations, and kill/recovery paths; bound through the Claude rule and the suggested `AGENTS.md` section. | | `config/dev-model.yaml` | #10 No hardcoding | The single config surface every skill and script reads instead of hardcoding a value. | | `scripts/check_doc_budget.py`, `scripts/archive_plan_sessions.py` | #1 | The tripwire and sweep that keep the handoff file from ballooning. | -| `scripts/pr_watch.py` | #5 | The poll-fix-ack engine behind `/pr-watch`. | +| `scripts/pr_watch.py` | #5 | The poll-fix-ack engine behind `pr-watch`. | | `scripts/dev_session.sh`, `scripts/reconcile_sessions.sh` | #3 | Worktree/lane launcher and reconciler. | | `scripts/hooks/pre-push` | #8 Mechanism over memory | A hook, not a memory — refuses a push that would corrupt the narrative files. | @@ -153,9 +171,10 @@ Principles #7 (model/effort tiering) and #9 (deterministic scaffolding around LLM steps) are doctrine woven into the skills and scripts above rather than a standalone file — read `PRINCIPLES.md` for both. -**Four skills ship wired; two ship as doctrine.** `session-start`, `wrap-up`, -`parallel`, and `pr-watch` come with their engine scripts and run out of the -box. `triage-friction-log` and `post-merge-systemize` document the flywheel's +**Four workflows ship wired for Claude and Codex; two ship as Claude-side doctrine.** +`session-start`, `wrap-up`, `parallel`, and `pr-watch` come with their engine scripts +and both runtime adapters. `triage-friction-log` and `post-merge-systemize` document +the flywheel's triage and pattern-finding mechanism, but their deterministic engines (a tracker client, a notify channel, and a merged-PR fetcher) are project-specific and left for you to wire — see the banner atop each of those two skill files. @@ -169,14 +188,14 @@ branch, and `DEVKIT_STATE_ROOT` state sandbox. The rule that makes it safe is **disjoint file footprints**: two lanes may run together only when no source file is edited by both (the sandbox prevents *state* collisions, not *source* merge conflicts). -The flow: `/parallel plan` clusters candidate work by footprint → launch a lane per +The flow: `parallel plan` clusters candidate work by footprint → launch a lane per disjoint cluster (`scripts/dev_session.sh new`) → each lane works to a draft-green PR → the cockpit reconciles every lane and merges. Each lane gets an effort tier and a merge class (self-merge vs operator-merge) assigned at plan time. Full walkthrough — the lane contract, the live board, reconciliation, and a worked example — in **[`docs/parallel-dev.md`](docs/parallel-dev.md)**. For step-by-step -recipes per use case (and what actually happens when you run each `/parallel` verb), +recipes per use case (and what actually happens when you run each `parallel` verb), see **[`docs/parallel-howto.md`](docs/parallel-howto.md)**. ## Adapting it @@ -185,8 +204,8 @@ Once you've adopted the kit, it's yours. `config/dev-model.yaml` is the single place to point the skills and scripts at your project's paths, tracker, review bots, and model tiers — start there. Beyond config, edit the skills and scripts freely: they're prompts and small stdlib scripts, meant to be read and changed. -The one module with a real test suite is `scripts/lib/state_paths/` — run its -tests (`python -m pytest scripts/lib/state_paths/tests/`) if you modify it. +Run the state-sandbox and portability suites after modifying the engines: +`python -m pytest scripts/lib/state_paths/tests/ scripts/tests/`. Improvements that would help other adopters are welcome back here. diff --git a/config/dev-model.yaml b/config/dev-model.yaml index 82a25e9..b115313 100644 --- a/config/dev-model.yaml +++ b/config/dev-model.yaml @@ -23,6 +23,17 @@ paths: friction_log: docs/friction-log.md # the friction inbox friction_log_archive: docs/friction-log-archive.md playbook: docs/autonomous-session-playbook.md # the autonomous operating contract + # Directory containing the deterministic kit engines. Mature repos can vendor + # them under scripts/devkit without rewriting every workflow. + engines: scripts + +runtime: + # Agent-specific adapters map the shared workflows onto their own command and + # skill surfaces. `none` prints activation instructions without an agent command. + default: claude # claude | codex | none | another configured key + launchers: + claude: claude + codex: codex doc_budgets: # Warn-only line-count tripwires (the archival mechanism of Principle #1). A file @@ -31,11 +42,11 @@ doc_budgets: - path: docs/handoff.md budget: 400 archive: docs/handoff-history.md - remedy: "run /wrap-up to sweep old session blocks into the history file" + remedy: "run the wrap-up workflow to sweep old session blocks into the history file" - path: docs/friction-log.md budget: 150 archive: docs/friction-log-archive.md - remedy: "run /triage-friction-log to graduate the inbox to the tracker" + remedy: "run the triage-friction-log workflow to graduate the inbox to the tracker" vcs: forge: github # github | gitlab | other — some skills branch on this @@ -56,23 +67,34 @@ tracker: label_name: "" review: - # Independent review present on this repo (Principle #5). `fallback_command` is the - # substitute pass to run when the bot is unavailable — a down bot is not a review - # waiver, it's a trigger to run the substitute. + # Independent review present on this repo (Principle #5). A down bot is not a + # review waiver; select the current runtime's substitute pass instead. bots: [coderabbit] - fallback_command: "/code-review" + fallback_commands: + claude: "/code-review" + codex: "/review" notify: backend: slack # slack | none — where skills DM the operator user_key: "" # a key into your project's own notify config models: - # The three-tier vocabulary (Principle #7). Names are illustrative — map them to - # whatever tiers your agent runtime exposes. Skills tag each step cheap/default/expensive - # and the tier travels with a delegated task. - cheap: haiku - default: sonnet - expensive: opus + # Runtime-neutral capability tiers (Principle #7). Workflows assign a tier; an + # adapter may translate it to a model, effort level, or leave it as guidance when + # the runtime cannot select compute per delegated task. + tiers: + cheap: mechanical + default: standard + expensive: judgment + runtime_mappings: + claude: + cheap: haiku + default: sonnet + expensive: opus + codex: + cheap: low + default: medium + expensive: high state: # The per-lane state sandbox (Principle #3) is driven by FIXED, project-neutral env diff --git a/docs/AGENTS-sections.md b/docs/AGENTS-sections.md new file mode 100644 index 0000000..50f9751 --- /dev/null +++ b/docs/AGENTS-sections.md @@ -0,0 +1,22 @@ +# AGENTS.md sections + +Copy and adapt this section into an adopting repository's `AGENTS.md`. Keep project +status in the configured handoff, not in `AGENTS.md`. + +## Agentic development workflow + +- Read `config/dev-model.yaml` before running a kit workflow; paths and runtime + mappings are configuration, not prompt literals. +- At the start of a development session, use the repository's `session-start` skill + to read the handoff, friction log, pull requests, CI, and tracker before choosing + work. +- After opening or updating a pull request, use `pr-watch` and continue until CI is + green and every review finding is fixed or explicitly answered. +- Use `parallel` only for lanes with disjoint source-file footprints. The cockpit + owns the configured handoff and friction log; lanes report through their PR bodies. +- At the end of every meaningful session, use `wrap-up` to update the configured + handoff, capture workflow friction, and leave one clear next step. +- For customer-facing gates, destructive operations, recovery paths, security work, + and changes to the lane-safety engines, read and apply + `docs/agentic-dev-kit/safety-critical-changes.md`. Green CI is necessary but not + sufficient; require independent review and operator sign-off. diff --git a/docs/CLAUDE-sections.md b/docs/CLAUDE-sections.md index d86abfa..c8203ac 100644 --- a/docs/CLAUDE-sections.md +++ b/docs/CLAUDE-sections.md @@ -65,7 +65,7 @@ infra failure) — then report the specific blocker and ask. **A review bot being unavailable is not a reason to skip review.** When a configured bot can't review an otherwise-ready PR — rate-limited, out of credits, or it silently skipped -the PR — run `review.fallback_command` as the independent review pass and triage its +the PR — run `review.fallback_commands.claude` as the independent review pass and triage its findings the same way (fix if confident and small, reply-with-reason otherwise). A blocked review bot is **not** a waiver: the "clean" bar still requires one independent review pass — bot *or* the fallback command. diff --git a/docs/agentic-dev-kit/safety-critical-changes.md b/docs/agentic-dev-kit/safety-critical-changes.md new file mode 100644 index 0000000..1997e55 --- /dev/null +++ b/docs/agentic-dev-kit/safety-critical-changes.md @@ -0,0 +1,37 @@ +# Safety-critical decision logic — review doctrine + +These files gate customer-facing sends, destructive operations, or process +kill/recovery paths. Four rules apply to any behavioral change here — each one earned +by a real shipped failure that CI-green + full unit tests did not catch (an +approval-matcher inversion; a send-gate with holes found only in review; a destructive +operation whose "safety" fix reintroduced the hazard; a kill-path that passed unit +tests but was broken in integration). See Principle #6 in `PRINCIPLES.md` for the +full doctrine this rule operationalizes. Agent-specific adapters should bind this +shared doctrine through `.claude/rules/`, `AGENTS.md`, or a triggered repository +skill; do not fork the doctrine into runtime-specific copies. + +1. **Deterministic gate > NLP/keyword matcher.** A matcher over free-text (approval + keywords, cancel phrases) is inherently leaky — repeated review rounds on a + leaky matcher each tend to find a *new* wrong-send, not close the class of bug. + When the decision matters, the durable design is a deterministic artifact (a + stamp, a state field, an explicit flag) written at decision time and verified at + act time. Treat "we tightened the matcher" as a stopgap, not a fix. + +1. **Dual-lens review for customer-facing gates.** One review pass — however strong — + is not enough: an adversarial/bypass-focused pass and a general-correctness pass + routinely find **disjoint** holes. A send/publish gate needs BOTH lenses before + merge. A single-lens "converged" verdict is an incomplete review, not a green + light. + +1. **Adversarial review to convergence, not one pass.** Re-review after every fix + round until a full pass finds nothing new. Fix rounds on gate logic routinely + introduce their own regressions — treat "the last round found nothing" as + provisional, not proof of safety. + +1. **Kill/recovery paths need an integration test.** Unit tests on the handler are + insufficient — a kill-path can pass unit tests while the wrapper-level behavior is + broken. Exercise the real signal/timeout/retry path (or a faithful harness of it) + before marking the change done. + +Merge class: changes governed by this rule are **operator-merge** — never self-merge +them from an autonomous or lane session, even when green and clean. diff --git a/docs/agentic-dev-kit/workflows/parallel.md b/docs/agentic-dev-kit/workflows/parallel.md new file mode 100644 index 0000000..ff58760 --- /dev/null +++ b/docs/agentic-dev-kit/workflows/parallel.md @@ -0,0 +1,418 @@ +# Parallel development + +Manage isolated **parallel dev sessions** — each one its own git worktree on a fresh +branch plus its own `DEVKIT_STATE_ROOT` sandbox, so several agent/dev sessions run at +once without clobbering each other's checkout or `state/cache/`. Thin in-session +front-end over the configured lane engine (the activation of the state-sandbox +primitive — see Principle #3 in `PRINCIPLES.md`). + +Read `config/dev-model.yaml` first. Resolve `` from `paths.engines`, +`` from `paths.handoff`, and `` from `paths.friction_log`. +Treat `cheap`, `default`, and `expensive` as neutral capability tiers and translate +them through `models.runtime_mappings` only when the current runtime supports that +control. A workflow invocation means `/name` in Claude or `$name` in Codex. + +Engine: `/dev_session.sh`. Sessions live in a +sibling `-sessions/` dir by default (override with `DEVKIT_SESSIONS_DIR`); +your CI/cron runner sets neither env var, so it's unaffected. + +## Default action — show the board + +With no argument (or `parallel list`), run and render the table of active sessions: + +```bash +/dev_session.sh list +``` + +Columns: `SCOPE · BRANCH · PR · CI (✓/✗/…) · DIRTY (uncommitted count) · SANDBOX +path`. This is the orientation surface when you're juggling several sessions — report +it as-is, then stop. Read-only. + +### Live board — `parallel list --watch [interval]` + +`list` is a one-shot snapshot. To *follow* in-flight lanes — CI flips, commits +landing, the DIRTY count moving, a PR going draft→ready or merging, or a silently-dead +lane that never moves — run the polling board instead: + +```bash +/dev_session.sh list --watch # re-render every 30s +/dev_session.sh list --watch 10 # …or every 10s +``` + +It re-renders on the interval and **marks with a leading `*`** (bold on a TTY) every +row whose state changed since the previous render — the change set is CI ✓/✗/…, a new +commit (HEAD moved), a DIRTY-count change, or a PR-state change (draft↔ready, review +decision). The first frame is the baseline (nothing marked); Ctrl-C stops it. On an +interactive terminal an unbounded watch repaints **in place** on the alternate screen +buffer (like `top`) — each frame replaces the last rather than scrolling a fresh copy +into your terminal history, and your pre-watch screen is restored when you Ctrl-C out +(a board taller than the window is clipped while watching, as with any full-screen +tool — use a taller window). A bounded `--max-iters` run instead leaves its final +frame on screen. The per-row SANDBOX cell is the compact `/state` tail (the +shared sandbox root is named once in the banner) so rows don't wrap on a long +absolute path — **this compaction applies to piped output too**; for the full +absolute sandbox path use bare `list`. Piped/redirected, the board is plain +escape-free text. Each `gh` lookup keeps the same short timeout as bare `list`, so a +slow network caps per-call and never hangs the loop. Use it as the cockpit's ambient +board while a batch is running rather than re-typing `list`. + +## Planning a batch — `parallel plan [focus]` + +When the operator wants to **start several sessions at once** (or asks "what could we +work on in parallel?"), don't jump to `new` ticket-by-ticket. The suitability test for +parallel work is **disjoint file footprints**: two sessions are safe together only +when no source file is edited by both. The sandbox makes concurrent `state/cache/` +*writes* safe — it does **nothing** for two branches editing the same source file +(that's a merge conflict + diluted review at PR time). So compose the batch +deliberately: + +1. **Orient.** `/dev_session.sh list` + `git worktree list` — active sessions + are file territory already claimed; exclude their footprints from the new batch. + +1. **Gather candidates.** Pull open tickets from your tracker (project + `tracker.project_name`, states In Progress + Todo) and the `▶ Next` deferred items + in ``. If a `` argument was given (a + theme, an area, or an explicit ticket list), scope to it. + +1. **Cluster by file footprint.** Group candidates by the files/dirs each one + touches — read the ticket and grep the code when unsure; don't infer the footprint + from the title. Present the clusters as a table. Within a cluster, pick **at most + one**; the rest go sequential. + +1. **Stale-premise pre-flight.** Flag any candidate whose fix may already be shipped + (checklist items matching recently-merged PRs; a "Done" tracker state that might be + a bot-driven auto-complete rather than actual code). Verify against the live code + **before** recommending it — premise-check-before-build is the house style. + +1. **Scope outward-safe.** A session that would push to an external system, send a + notification, or post to a customer-facing channel gets scoped to its **in-repo / + authoring half**; the gated outward step stays an operator action after merge. + +1. **Assign an effort tier.** The risk read you just did (cluster + stale-premise + + outward-safe) also sets **how much reasoning each lane gets** — tag each chosen + lane `low` / `medium` / `high` (→ `max` for the gnarliest) from the [lane-risk → + effort tier map](#per-lane-effort-tier-risk--reasoning-effort--model) below, so the + launch step can resource it. + +1. **Recommend + confirm.** Propose a disjoint batch (one ticket per cluster) plus the + residual shared-file watch-outs (e.g. two tickets that *might* both touch the same + schema file), then let the operator choose the set. Tag each proposed lane with + **both** its effort tier and its [merge class](#per-lane-merge-class-self-merge-vs-operator-merge) + (self-merge / operator-merge) up front — deciding the merge boundary at plan time, + not at merge time, stops a batch stalling on ad-hoc "can I merge this?" calls and + tells each lane whether it may self-merge or must hand back. + +1. **Launch each + relay a kickoff.** Run `/dev_session.sh new ` per + chosen ticket (see below) and relay each copy-paste line **with a kickoff prompt** + the operator pastes as the session's first message: + + > Read tracker ticket `` (+ any recipe in ``). Pre-flight its + > premise against the live code before coding. Branch `dev/` is ready. + > **Suggested effort: ``** (``) — set your session's + > model (and reasoning effort, if your client exposes that control) accordingly + > before starting. Draft PR on first push → mark ready when done → `pr-watch` to + > green. Heads-up: a parallel session owns `` — if you need to touch + > ``, flag it before committing. **Do not edit `` or + > ``** — those are cockpit-owned; put your handoff (what + > shipped, lessons, deferrals) in the **PR body** so the joint wrap-up can + > aggregate it. + + Interactive `new` prints the configured agent CLI command for the **operator's own + shell**, so it can only *suggest* the tier — the operator applies it (model and + reasoning effort, if exposed) when starting the session. An **unattended/headless** + launcher may set the tier when its runtime exposes that control — see + [Unattended / headless launch](#unattended--headless-launch--new---headless). + +After launch, **this** session is the cockpit: `list` (or `list --watch` for an +auto-refreshing board that flags each CI/commit/PR transition) is the live board, and +you sequence (merge → rebase) any two PRs that end up sharing a file. Two-collision +example worth stating to the operator up front: if two batched tickets both live under +one package but edit different files, they're safe — name the one file (often a +shared schema module) that would force them sequential if both need it. + +### Per-lane effort tier (risk → reasoning effort + model) + +A mechanical doc lane shouldn't burn max-effort on your most expensive model, and a +shared-primitive lane shouldn't run at low effort. Map each chosen lane to a tier from +its risk: + +| Lane risk | Typical work | Effort | Model | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------- | -------------------------------------------- | +| **Mechanical** | doc/comment-only edits, rename, copy tweak, config-value bump, dead-code delete, a `_latest`-rename sweep | `low` | inherited (`cheap` is fine) | +| **Standard** (default) | a normal feature/bugfix scoped to one package, test additions, a self-contained script | `medium` | inherited (`default`) | +| **High-stakes** | schema / data-shape change, a shared primitive (a state-sandbox library, a pipeline-state helper), a guard / gate / verifier, security, anything touching production cron/CI or shared `state/`, the merge-rules / scoring core | `high` (→ `max` for the gnarliest) | `expensive` | + +When unsure, round **up** — under-resourcing a risky lane costs a bad merge; +over-resourcing a cheap one costs only tokens. + +### Per-lane merge class (self-merge vs operator-merge) + +The same risk read that sets the effort tier also sets **who lands the PR** — +pre-classify it at plan time so a green lane isn't left waiting on an ad-hoc merge +decision (and so an autonomous batch knows what it may close itself): + +| Lane risk | Merge class | Who merges | +| ------------------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------ | +| **Mechanical** / **Standard** | **self-merge** *(autonomous/headless batches only)* | the lane (or cockpit), once green-and-clean with one independent review pass | +| **High-stakes** | **operator-merge** | always the operator — never self-merged, even when green | + +Operator-merge is the floor for anything in your project's high-risk classes: +data-shape / fetcher / config-semantics changes, shared primitives, a **guard / gate / +verifier / send-path / kill-path** (see + `docs/agentic-dev-kit/safety-critical-changes.md`), security, PII, +or anything touching production cron/CI or shared `state/`. Normal **interactive** +sessions leave *all* merges to the operator regardless of class — self-merge is an +autonomous-session behavior (see + the configured autonomous-session playbook (`paths.playbook`). +When unsure, classify **operator-merge**. + +**How the tier reaches the lane.** It depends on the launch mechanism: + +- **Headless lanes — fan out through a multi-agent workflow when it exposes a real + effort dial.** Such a launcher passes each lane its + tier directly — one sub-agent per lane, each given its own `effort`/`model` — so + **both** halves of the tier (reasoning effort *and* model) actually take effect. + Use this path when the runtime supports it and lanes are tiered differently. Recipe + caveats in [Unattended / headless + launch](#unattended--headless-launch--new---headless). +- **Headless lanes — a single background sub-agent per lane is the *model-only* + fallback.** If your agent runtime's background-task tool exposes `model` but no + `effort` parameter, spawning lanes as individual sub-agents sets each lane's *model* + per the tier while the *effort* reaches it only as a prose hint in the prompt + ("tier: high, think carefully") — nominal, not a real setting. Fine when there's a + single lane, when every lane shares a tier, or when you want independently-stoppable + cockpit-side agent objects; use the workflow-fan-out path when per-lane effort + differentiation is the point. Either way the tier is **sourced from the plan's risk + assessment**, one per lane. +- **Interactive `new` lanes**: the kickoff only *suggests* the tier (above); the + operator sets their own session's effort/model. + +**Default-safe.** A lane with no assigned tier inherits the cockpit's current +effort/model — i.e. unspecified ⇒ today's behavior, no regression. The tier is an +*optimization* of a working default, never a prerequisite. + +### Joint wrap-up — the cockpit owns the handoff + +`` and `` are shared *narrative* files: a +per-session edit to either collides at merge **and** pollutes a focused code PR, so +sessions never touch them (the kickoff says so). Each session's handoff rides its +**PR body** — the one channel that's committed, reviewed, and visible across +worktrees (dev-session `state/` sandboxes are isolated by design, so a session's +worktree scratch is invisible to the cockpit). When the batch is closed — every PR +merged or consciously parked — run the joint wrap-up **from this cockpit session**: + +1. **Reconcile every launched scope to a terminal state first — before reading any + narrative or writing any block.** An aggregate "all merged" is *not* evidence a + scope shipped: a silently-dead session (never started, 0 commits, no PR, branch + still at the base tip) can get closed as done if nothing checked per-branch. Run + the reconciler over the batch's launched scopes: + + ```bash + /reconcile_sessions.sh + ``` + + For each `dev/` it resolves a **merged** PR (`gh pr list --head dev/ + --state merged`) or marks it **parked** with the reason (`EMPTY — 0 commits, never + started`, `PR closed unmerged`, `N commit(s), no PR opened`) or **open** (still in + flight), then prints the `launched N, merged M, parked K` tally — exit 3 if any + scope is open or parked, 0 only when all merged. **Do not write the wrap-up block + until every launched scope is merged or consciously parked.** A scope that + reconciles to **open** means the batch isn't closeable — finish or park it first. A + scope that reconciles to **parked** gets named as parked in the block, never folded + into "all shipped". (Pass the scopes explicitly — `rm` removes session dirs, so a + scope already torn down won't auto-discover; the cockpit knows the launched set.) + + The reconciler is **mechanism-agnostic** — it keys on branch / PR head ref, so it + also covers batches *not* launched via `parallel` (a background sub-agent fan-out, + headless lanes). For those, pass branches directly or a glob instead of scopes: + + ```bash + /reconcile_sessions.sh --match 'feat/login-*' # every local+remote branch matching the glob + ``` + + With no args it discovers in-flight lanes from **both** session dirs and live `git + worktree`s (deduped by branch), so a background-sub-agent worktree gets the same + `launched/merged/parked` net that catches a dead session. + +1. Read each **merged** PR's narrative: `gh pr view --json title,body` per merged + batch PR (parked scopes have no landed narrative to read). + +1. Write **one** "Latest session" block for the whole batch via `wrap-up` — open + with the `launched N, merged M, parked K` line, then PRs landed, collisions + avoided, and each parked scope with its reason. Not one block per session. + +1. Open it as its own `chore: update handoff` PR (this checkout sits on the protected + branch, so the handoff edit goes through a branch + PR like everything else; + mirrors the existing `chore: update handoff` cadence). + +Overflow that doesn't fit a PR body (richer lessons, friction-log entries) goes to a +uniquely-named `docs/handoff/.md` fragment that rides the session's **own** PR +— disjoint path, zero collision — and the cockpit folds it into the wrap-up block and +deletes it. Start with PR-body-primary; only reach for fragments when bodies prove too +thin. + +## Starting a new session + +An interactive session must be launched from the operator's **own shell** (an +in-agent workflow cannot `cd` the operator into a new worktree and open a fresh +terminal there). When asked to prepare one, run: + +```bash +/dev_session.sh new +``` + +substituting a lowercase slug for `` (e.g. `feat-graduation-flow`). Pass +`--runtime ` to select a configured launcher or `--launcher ` to +override it for this lane. The script +prints a copy-paste line — `cd && export DEVKIT_STATE_ROOT=… && export +DEVKIT_ROOT=… && `. **Relay that line to the operator** and tell them +to run it in a new terminal; don't try to start the session yourself. Options: +`--base ` (default `main`), `--prefix

` (default `dev` — parallel-session +branches get their own namespace to avoid colliding with hand-named feature branches), +`--branch ` to override the whole name. + +### Unattended / headless launch — `new --headless` + +Interactive `new` is operator-launched by design (it prints a copy-paste line and the +rule above says *don't start the session yourself*). That's the wrong shape for an +**unattended** batch — a background sub-agent or a cloud session that should drive a +*sandboxed* lane without a human in the loop. `--headless` is for exactly that: + +```bash +/dev_session.sh new --headless +``` + +It creates the worktree + sandbox exactly as `new` does, but instead of the human +block it: + +1. **Writes a sticky `/.devkit_state_root` marker** holding the absolute + sandbox path. This is the mechanism that makes a headless lane safe: a background + sub-agent's shell calls don't share a shell, so an exported `DEVKIT_STATE_ROOT` + doesn't survive call-to-call. Your state-sandbox resolver reads the marker + (walking up from cwd) when the env var is unset, so the lane's `state/` writes + isolate into the sandbox **automatically** — no env gymnastics in the prompt. + (Precedence: env var → marker → repo-root default. Cron/CI writes no marker, so + it's unaffected.) +1. **Prints a JSON descriptor to stdout** (diagnostics go to stderr, so stdout is + clean JSON): `{"scope","branch","worktree","state_root","repo_root","base", + "prompt_preamble","env","runtime","launcher"}`. `prompt_preamble` is the canonical lane-contract text + below — the launcher **MUST** prepend it verbatim to the lane's task prompt. `env` + (currently `{"DEVKIT_REFUSE_UNSANDBOXED_STATE": "1"}`) flips the unsandboxed-write + guard from *warn* to *refuse*, by default, for every headless lane — so a lane + whose marker resolution somehow fails (deleted marker, cwd escaped the worktree) + hard-errors on a `state/` write instead of silently landing in prod. Interactive + `new` and cron/CI never set either field. + +### The lane-contract preamble (inject this verbatim) + +Every mechanism that hands a task prompt to a headless lane — a multi-agent +workflow fan-out, a single-background-sub-agent fallback, or any future launcher — +**MUST prepend the same fixed contract text** ahead of the task-specific +instructions. This is the fix for an idle-stall failure mode: a rule that lives only +in a memory or in this doc's prose can't bind a freshly spawned lane, because a fresh +agent has no memory and doesn't read `parallel.md` unless told to. The contract must +be *in the prompt itself*, every time. + +Fetch the current text with `/dev_session.sh print-contract` (plain text, no +JSON) or read it straight off the `prompt_preamble` field of any `new --headless` +descriptor — **do not hand-copy or paraphrase it into this workflow or a launcher**. +Always read it fresh from one of those two engine surfaces so a future edit propagates +without maintaining a second copy. + +**Launch contract (cockpit usage).** Every supported launcher drives the same `new +--headless` worktrees. Each launcher **MUST prepend the lane-contract preamble to +every lane prompt**; runtime capability changes how tiers are applied, not whether +the safety contract binds. + +**Preferred when available — a workflow launcher with a real effort dial.** Run +`new --headless ` once per chosen scope, collect each one's JSON descriptor +into a list (attaching the per-lane `effort`/`model` tier from the plan's risk read), +then drive the lanes from a *single* fan-out that gives each sub-agent its own +`{effort, model}` — the one path on which the tier's `effort` half actually takes +effect. Pseudocode: + +```js +// args.lanes = [{scope, worktree, branch, ticket, effort, model, prompt_preamble}, …] +// — one per `new --headless` descriptor (prompt_preamble copied straight off it). +// effort ∈ low|medium|high|max (omit ⇒ inherit cockpit effort); model ∈ cheap|default|expensive (omit ⇒ inherit). +runInParallel(args.lanes.map(lane => () => + spawnAgent( + `${lane.prompt_preamble}\n\n` + + `Work in worktree ${lane.worktree} on branch ${lane.branch} (cd there first — its state sandbox is active via the on-disk marker, so your state/ writes isolate automatically). ` + + `Read tracker ticket ${lane.ticket}, pre-flight its premise against the live code, implement, draft PR on first push, drive it to green-and-clean, then hand off per the contract above.`, + { label: lane.scope, effort: lane.effort, model: lane.model } // omit effort/model to inherit (default-safe) + ) +)) +``` + +Four things to keep right: **(1)** `lane.prompt_preamble` is prepended verbatim, +ahead of everything else, on every lane — never abbreviated to "follow the usual +contract" (that's exactly the prose-reference that failed to bind a lane before). +**(2)** do **not** open a second worktree on top of `--headless` — it already owns the +worktree+sandbox, so a second one would have no marker and lose isolation. **(3)** A +lane with no assigned tier omits `effort`/`model` and inherits the cockpit's — the +same default-safe fallback as everywhere else. **(4)** Check what compute budget your +fan-out mechanism draws from before running a large batch, and monitor it via +whatever live-progress view your runtime exposes, plus `list --watch` on the lanes' +branches/PRs. + +Note on the `env` field: if your fan-out or background-agent tool doesn't expose a +parameter to inject env vars into the spawned lane's process, `DEVKIT_REFUSE_ +UNSANDBOXED_STATE` cannot be force-set through that launch path the way +`prompt_preamble` can be force-injected into the prompt text. In the normal case this +is moot: the marker already resolves the sandbox, so the guard never fires. The `env` +field exists for launchers that DO control the spawned process's environment (a +custom subprocess-based batch driver) — read it and export it there. Treat this as a +documented, conservative gap, not a silent one. + +**Fallback — a single background sub-agent per lane (model-only).** Parse the +descriptor and spawn a background sub-agent whose prompt is **the `prompt_preamble` +field, prepended verbatim**, followed by the task-specific instructions naming the +`worktree` path — e.g. *"`` Work in worktree `` on branch +``. The state sandbox is active via its on-disk marker — your `state/` writes +isolate automatically. Read tracker ticket ``, pre-flight its premise, draft PR on +first push, drive it to green-and-clean, then hand off per the contract above."* Same +no-second-worktree rule and same prepend-verbatim requirement as the workflow path. +This path sets each lane's `model` per the tier but **not its effort** if your +runtime's background-task tool has no effort dial — fine for a single lane, lanes that +all share a tier, or when you want individually-stoppable cockpit-side agent objects. +The how-the-tier-reaches-the-lane mechanics + the default-safe fallback live in +[Per-lane effort tier](#per-lane-effort-tier-risk--reasoning-effort--model). + +**When to use which.** Attended work (operator at a terminal) → plain `new`. +Unattended pipeline-touching work (any lane that writes `state/cache/`) → `new +--headless` so the sandbox is active without a surviving shell export. This is the +`parallel` vs bare-background-agent decision rule: *does the lane write `state/`? → +it needs a sandbox → `new --headless`, not a bare background worktree.* This should be +**guarded, not just documented**: your state-sandbox write path should warn when an +unsandboxed lane (no `DEVKIT_STATE_ROOT`, no marker, job-name env unset, in a linked +worktree) writes repo-root `state/` — and `new --headless` sets +`DEVKIT_REFUSE_UNSANDBOXED_STATE=1` by default (the `env` descriptor field + activate +snippet above) to make that a hard error rather than a warning. Cron/CI and normal- +interactive paths are unaffected. + +## Finishing a session + +After its PR has merged: + +```bash +/dev_session.sh rm +``` + +Removes the worktree + sandbox; deletes the branch only if it's merged into the +protected branch (kept with a warning otherwise). Refuses if the worktree has +uncommitted changes unless `--force`. `/dev_session.sh path ` prints the +worktree path (handy for `cd "$(/dev_session.sh path )"`). + +## Notes + +- **Why the sandbox.** Writes from a session (any skill/script that writes `state/`) + land in that session's sandbox; shared-cache *reads* take the newer of + sandbox-vs-main so a session still sees fresh prod caches read-only. This is what + lets two sessions run data skills concurrently without corrupting `state/cache/`. + See Principle #3 in `PRINCIPLES.md`. +- **Pairs with** `session-start` (orient within one session) and your project's + branching convention (draft PR → ready → watch-and-fix). `parallel` is the + *across-sessions* view; `session-start` is the *within-session* one. +- Read-only by default (`list`; `plan` is read-only until the operator confirms a + batch). `new`/`rm` mutate worktrees only — never the repo's own tree, never prod + `state/`. diff --git a/docs/agentic-dev-kit/workflows/pr-watch.md b/docs/agentic-dev-kit/workflows/pr-watch.md new file mode 100644 index 0000000..fd65e91 --- /dev/null +++ b/docs/agentic-dev-kit/workflows/pr-watch.md @@ -0,0 +1,98 @@ +# PR watch + +Watch a pull request until it is **green and clean** — CI fully passing and every +review-bot / human review finding fixed or replied-to — then report. This is the loop +your project's "PR follow-through" policy mandates after opening or pushing to a PR; +run it without being asked. + +**Input:** an optional PR number. With none, the current branch's open PR is used. + +Read `config/dev-model.yaml` first. Resolve `` from `paths.engines`, +and select the current runtime's independent fallback from +`review.fallback_commands` when needed. + +If the diff affects a customer-facing gate, destructive operation, recovery path, +or other configured high-risk file, read and apply +`docs/agentic-dev-kit/safety-critical-changes.md`. Green CI alone is not merge +approval for that class. + +Engine: `/pr_watch.py` (deterministic — check rollup + comment union across +issue/review/inline surfaces, noise-filtered, diffed against a per-PR seen-set). You +drive the loop + apply the judgment. + +## Loop + +Repeat until the report says **done**: + +1. **Poll.** `uv run /pr_watch.py --json` (omit `` for the current + branch). Read `done`, `checks` (`all_green`, `failing[]`, `pending`), and + `new_comments[]`. + +1. **If `done` (checks all green + nothing new):** stop the loop and report — PR #, + the green check count, and "no outstanding review findings." You're finished. + +1. **If checks are still `pending` and there are no new comments:** nothing to do yet + — wait and re-poll (see Pacing). CI can take 20–30 min; that's expected, keep + going. + +1. **If a check is `failing`:** investigate (`gh run view --log-failed`, or + `gh pr checks `), fix the cause in the code, run your project's local gate + (e.g. `make check`), commit, and `git push`. The push re-triggers CI — keep + looping. + +1. **If there are `new_comments`:** handle each with judgment — + + - **Real finding** (a bug, a missing guard, a correctness/clarity issue): fix it in + the code, commit, push. Re-running the local gate first. + - **Nitpick you disagree with** (style preference, out-of-scope, already-correct): + **reply with a brief reason** rather than changing code — `gh pr comment + --body "..."` for a top-level reply, summarizing what you addressed vs. skipped + and why. + - Verify each finding against the *current* code before acting — some go stale + across rounds (a later commit already fixed it). + +1. **Acknowledge the round:** once you've handled this round's findings, run `uv run + /pr_watch.py --mark-seen` so they don't resurface. `--mark-seen` never + re-polls `gh` — it promotes the exact set of comment keys that your last `--json` + poll reported (persisted locally in the per-PR state file as a "pending" set) into + the seen-set, then clears it. This makes the ack deterministic: a comment that + lands on the PR *after* your read-poll and *before* `--mark-seen` was never part of + that pending set, so it can't be acked by this call — it stays unseen and surfaces + on your next poll instead of being silently buried. Calling `--mark-seen` without a + prior poll (nothing pending) acks nothing and says so (`note` in the output) — + always poll-and-read first. + +1. **Pace the next poll** (see below), then go to step 1. + +## Pacing + +Self-pace on a bounded cadence — don't busy-wait: + +- **Review bots** land their first pass ~2–5 min after a push. Poll ~every 180–270 s + while waiting on them (stays inside the prompt-cache window). +- **CI** can run 20–30 min. While only checks are pending, a longer 300–600 s cadence + is fine. +- After you push a fix, expect a fresh CI run + possibly a re-review — keep looping; + don't declare done off a stale poll. + +## Stop conditions + +- **Done** — `done: true` (green + clean). Report and finish. This is the goal. +- **Stuck / needs a decision** — a check fails for a reason you can't resolve (a + flaky-infra failure that won't clear on re-run; an external dependency; a finding + that needs an operator product/design call). Stop, report the specific blocker, and + ask. Don't loop forever on something only the operator can unblock. +- **Bound the loop** — if you've gone ~8–10 rounds without converging, stop and + summarize where it stands rather than looping indefinitely. + +## Notes + +- The seen-set lives at `state/pr-watch/.json` (gitignored). It's per-PR, so + re-running on a different PR starts fresh. +- Known auto-noise from your review bots (billing/usage notices, walkthrough / "no + actionable comments" summaries) is filtered out by the engine — edit the + noise-marker list in `/pr_watch.py` for your own bot mix; those never count + as findings. +- This is interactive-only. A scheduled job that opens its own PRs should be excluded + from this loop by your cron/CI runner's job-name signal, so an automated open never + silently enters an unattended watch loop. diff --git a/docs/agentic-dev-kit/workflows/session-start.md b/docs/agentic-dev-kit/workflows/session-start.md new file mode 100644 index 0000000..059b5ca --- /dev/null +++ b/docs/agentic-dev-kit/workflows/session-start.md @@ -0,0 +1,183 @@ +# Session start + +Start-of-session briefing — the bookend to `wrap-up`. Reads the living handoff, the +friction-log inbox, your tracker, and live repo/CI state, then proposes **what to do +next**: candidates grouped by **urgency** and tagged `[size · model · mode]`, ending +with one recommendation. + +## Resolve configuration + +Read `config/dev-model.yaml` first. In this workflow: + +- `` and `` mean `paths.handoff` and `paths.friction_log`. +- `` means `paths.engines`. +- `cheap`, `default`, and `expensive` are the neutral keys under `models.tiers`. + Apply the current runtime's `models.runtime_mappings` value only when the runtime + can actually select a model or effort level for that step. +- A workflow invocation means the current agent's native adapter: `/name` for the + shipped Claude commands or `$name` for the shipped Codex skills. + +## What it reads + +| Source | How | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| Living handoff | `` — latest session block + every "Next:" / "Follow-ups:" trail | +| Friction inbox | `` — entries since the last "Backlog migrated" marker | +| Tracker backlog | your tracker's list-issues command/script — project `tracker.project_name` (open only — drop `completed`/`canceled`) | +| Open PRs | `gh pr list` — anything draft / CI-red / awaiting-review (the PR-follow-through rule) | +| Working tree | `git status --short` + `git branch --show-current` — unfinished business from last session | +| CI/cron health | your cron/CI runner's status command (adapt to your infra — e.g. a wrapper script that logs recent job outcomes) | +| Config drift | your host config-apply step, if you have one (e.g. a `verify --json`-style check comparing committed config against applied host state) — drop this bullet entirely if it doesn't generalize to your setup | + +### 0 · Gather (run in parallel) + +Fire these together — they're independent: + +- `git status --short` and `git branch --show-current` +- `gh pr list --state open --json number,title,isDraft,reviewDecision,statusCheckRollup,author --limit 20` (`author` distinguishes a **cron/automation-opened** PR from one a person opened; those are guarded out of `pr-watch` by your cron runner's job-name signal, so their bot findings get no automated follow-through and the next cockpit must adopt them — see Step 2) +- your cron/CI health command (adapt to your infra) +- your config-drift check, if you have one (parse its output for a 🔴-worthy line in Step 2) +- Read `` (focus: the **"Latest session"** block and its `Next:` / `Follow-ups:` lines, plus the top-of-file "Last updated" trail for the active sprint) +- Read `` (the inbox — entries above the most-recent `## … — Backlog migrated to ` marker; everything below it is already ticketed) +- **Tracker** (optional — if the script/key fails, note the gap and continue): a field-limited list-issues call against `tracker.project_name` (id/identifier/title/url/state/priority/updated — avoid pulling full descriptions, which is what makes a naive "dump everything" call routinely overflow a tool's token limit). Discard issues whose state type is `completed`/`canceled`, and print a compact table sorted urgent(1) → low(4) with no-priority(0) last. A missing/invalid config or missing tracker credential should exit non-zero with a clear message — treat any non-zero exit as the optional-tracker gap (note it and continue) — never act on a partial payload. + +### 1 · Classify each candidate + +Turn the raw signals into a deduped candidate list. Each candidate gets an **urgency +bucket**, a **size**, a **model tier**, an **execution mode**, and a **source +pointer**. + +**Urgency** (the grouping axis): + +- 🔴 **Now** — broken or actively in-flight. A CI/cron failure on an active job that + needs recovery; an open PR that's CI-red or has unaddressed review (opening/pushing + a PR isn't done — watch-and-fix is the same task) — **including a cron/automation- + opened PR** (identified by `author` in the gather) with a changes-requested decision + or unresolved bot findings: your cron runner's job-name guard means `pr-watch` + never watched it, so adopting it (run `pr-watch `) is this session's job; + uncommitted work from last session that should be finished or committed; the + handoff's explicit current `Next:` **iff** it's the active sprint's blocking step; + any entry your config-drift check flags — a merged config change that's inert on + the host until applied is an **operator host-action** reminder, not a delegatable + build candidate — render it without model/mode tags (see Step 2). +- 🟡 **Soon** — this week's clear next steps. Active-sprint follow-ups, time-bound + items ("validate Wednesday's run"), medium-severity friction-log entries, + started/high-priority tracker tickets. +- 🟢 **Whenever** — backlog. Low-severity friction-log entries, lower-priority tracker + tickets, nice-to-haves. + +**Size** `S / M / L` — scope/effort: `S` ≤ one small PR (~30 min); `M` a focused +single-concern PR (~an hour); `L` multi-PR or a sprint slice. + +**Model tier** — the *intelligence* the work needs (orthogonal to size — a large +mechanical sweep is `L · cheap`; a one-line calibration decision is `S · expensive`). +The three tier names below are runtime-neutral. Use the current runtime's mapping +when it exposes model or reasoning-effort controls; otherwise keep the tier as +planning guidance. **Default to the middle tier** for well-specified, self-contained +work. Reserve the top tier for genuinely tough problems and the cheap tier for +purely mechanical work: + +- **`cheap`** — purely mechanical / deterministic: renames, dead-code removal, + config sweeps, applying a known one-line fix. No grounding or judgment needed. +- **`default`** — **the default tier.** Self-contained build / refactor / doc + work with clear acceptance criteria, *including* work that must verify itself + against live code/schema; given a precise spec it produces top-tier-grade output. + This is the default `delegate` tier. +- **`expensive`** — reserve for the *really tough* tasks: design decisions, + calibration / threshold choices, ambiguous or emergent scoping, security-sensitive + changes, cross-system reasoning — anything where being wrong is expensive and hard + to catch in review. These almost always run `inline` (expensive tier + judgment ⇒ + inline), not delegated. + +**Execution mode** `inline / delegate` — *where* the work runs once you greenlight it; +the token lever (a plan, not an action — nothing launches until the operator picks one +in Step 3): + +- **`delegate`** — use the current runtime's isolated-task mechanism for + **self-contained, clearly specified work**, while the cockpit retains orchestration + and review. Request the mapped tier only when that mechanism supports it. Drop to a + cheap-tier delegate only for purely mechanical sweeps; an expensive-tier delegate + is rare because high-judgment work usually benefits from live steering. +- **`inline`** — do it in this session. Right when the item is **expensive-tier** + (high judgment), needs **live iteration / your input as it unfolds**, or is + **exploratory** (scope emerges as you go). If you want a cheaper tier for an inline + item, adjust the session's model or effort only when the runtime exposes that + control; live steering matters more than the exact setting. + +Rule of thumb: `self-contained + clear spec ⇒ delegate to default` (cheap-tier +only for purely mechanical sweeps); `really tough / high-judgment, or +interactive/exploratory ⇒ inline on expensive`. When in doubt, default `inline` +(no regression vs today). + +**Source pointer** — every item shows where it came from so you can drill in: +`handoff`, `friction-log `, a tracker ticket id, `PR #NNN`, or the job name. + +**Rules:** + +- **Dedup.** An item that appears in more than one source (a friction-log entry that's + also a tracker ticket, say) is listed **once**, with the most authoritative pointer + (tracker > handoff > inbox) and any others noted inline. +- **Don't invent work.** Only surface candidates traceable to one of the sources + above. If a source is empty or clean, say so — a quiet bucket is a real result. +- Keep each line to one sentence of *what* + the tags + the pointer. No essays. +- **Composing an autonomous/overnight self-merge batch:** also tag each lane with its + predicted merge class (self-merge / operator-merge — see `parallel`'s per-lane + merge-class table) and report the split to the operator before launch — e.g. "2 will + self-merge, 3 held for you." + +### 2 · Render the briefing + +```text +🧭 Session Start — + +Where things stand + • () · open PRs · CI/cron: + • Active sprint: + • Last session: + +What to do next + +🔴 Now + • [ · · ] + • N config change(s) INERT pending a host apply step — , , … +🟡 Soon + • [ · · ] +🟢 Whenever + • [ · · ] +``` + +- Omit a bucket entirely if it's empty (don't print "🔴 Now: nothing"), but if **all** + of Now+Soon are empty, say so plainly — e.g. `✅ All clear — nothing urgent or due + this week; see 🟢 Whenever for backlog.` +- Order items within a bucket by leverage (blocking > high-value > cheap-win). +- The config-drift line only appears when your drift check reports something + outstanding; name the affected items. A less-urgent "orphan" class of drift (config + present with nothing applying it, or vice versa) is a separate, lower-urgency + concern — mention it only under 🟢 Whenever if present, never conflated with the 🔴 + line above. + +### 3 · Recommend one, then wait + +End with a single pick and a one-line why, then **stop** — let the operator choose. Do +not auto-start the work. + +```text +👉 My pick: [ · · ] — + + Want me to start it, or pick another? +``` + +Rationale heuristics: prefer 🔴 Now if the bucket is non-empty; otherwise the active +sprint's blocking next step; break ties toward the highest value-per-effort (small + +high-leverage). + +## Notes + +- **Read-only.** This skill never edits, commits, or starts work — it only reports + and recommends. It's safe to run anytime to re-orient mid-session. +- If `` or `` is over its line budget (a + session-start tripwire may have warned), mention it as a 🟢 housekeeping item + (`wrap-up` sweeps the handoff; the `triage-friction-log` workflow graduates the inbox) — don't + sweep inline. +- Pairs with `wrap-up` (session end). Use `pr-watch` to action a 🔴 PR item, + the `triage-friction-log` workflow to clear the inbox. diff --git a/docs/agentic-dev-kit/workflows/wrap-up.md b/docs/agentic-dev-kit/workflows/wrap-up.md new file mode 100644 index 0000000..8f5e5ce --- /dev/null +++ b/docs/agentic-dev-kit/workflows/wrap-up.md @@ -0,0 +1,86 @@ +# Wrap up + +End-of-session wrap-up. Update the living handoff and commit. + +## Resolve configuration + +Read `config/dev-model.yaml` first. In this workflow, ``, +``, and `` mean the corresponding values under +`paths`; `` means `paths.engines`. A workflow invocation means the +current agent's native adapter (`/name` in Claude or `$name` in Codex). + +## Steps + +1. **Read the current handoff** — ``. + +1. **Review what changed this session** — check `git diff` and `git log` since the + handoff's "Last updated" date + +1. **Update ``**: + + - Move completed work from "In Progress" to "Done" (with a one-line summary of + what shipped) + - Add any new items discovered during the session to the appropriate section + - Update sprint status if a sprint boundary was crossed + - Remove resolved housekeeping items + - Update the "Last updated" date to today + - Keep it concise — the handoff is a handoff document, not a changelog + +1. **Capture friction** — if this session surfaced any bug, friction, or idea specific + to a workflow (a skill, a cron/CI job, a pipeline), append a short entry to + `` under a dated `## YYYY-MM-DD` + heading: the observed issue, a severity (**H**/**M**/**L**), and a proposed fix. + This is the documented session-end practice — it captures learnings while fresh; + they later graduate to your tracker via the `triage-friction-log` workflow. Add to the inbox + only — don't graduate or sweep here. Skip if nothing workflow-specific came up. + +1. **Suggest a next-session starter** — if the session ends with a *clear* follow-up, + hand the next session a running start: + + - **One obvious next thing** → add it as a final `▶ Next: ` line at the end + of the latest session block in ``, and print the same starter in + the chat. Make it concrete and copy-pasteable: a native workflow invocation or a + one-line task prompt that names the file / ticket / PR (e.g. `▶ Next: pr-watch + 1131 — fix review findings then self-merge`). The `▶ Next:` line is an allowed + addition (like the archive sweep), not a structure change to ask about. + - **Diffuse / several threads** → don't invent a false single thread; tell the + operator to open next session with `session-start` (it re-reads handoff + + inbox + tracker + live repo/CI state and re-proposes what to do). + - **No clear follow-up** → skip this step. + +1. **Update any project-status doc** (e.g. a dashboard snapshot) if any metrics + changed this session — adapt this step to whatever presentation artifact your + project keeps; skip if you don't have one. + +1. **Keep the handoff docs lean.** After adding this session's block, run + `uv run /check_doc_budget.py`. If it warns that `` is over + budget, run `uv run /archive_plan_sessions.py` — it deterministically keeps + the newest ~6 session blocks live, moves the rest into ``, + and trims the megaline. Stage **both** files (`` + + ``) into this commit. If `` is over + budget, don't sweep it inline — note it and recommend the `triage-friction-log` workflow + (graduating the inbox needs tracker writes + operator approval). This is what + stops the handoff docs from ballooning between archive sweeps. + +1. **Commit + PR the handoff update — never commit to your protected branch + directly.** Commit as `chore: update handoff — [one-line summary of session work]` + (stage `` too if you added an inbox entry this session, and + `` if the archive sweep ran). If you're already on the + session's feature branch, this is just another commit on that branch's PR. If + you're on the protected branch (e.g. a planning-only session), branch first + (`chore/update-handoff-`) before committing, then push and open a PR. Either + way, once there's nothing left to push, **mark the PR ready** so it gets reviewed, + and run the watch-and-fix loop (`pr-watch`) to merge — per your project's + branching convention. + +## Rules + +- Do NOT add session-specific detail (decisions, debugging steps, conversation + context) — that belongs in session-scoped scratch notes or memory, not the living + handoff +- Do NOT change the handoff's structure or add new sections without asking — but the + `archive_plan_sessions.py` sweep (moving old session blocks to ``) + and a single `▶ Next:` starter line at the end of the latest session block are both + documented additions, not structure changes, so do them without asking +- If a backlog item was promoted to a sprint epic, move it (don't duplicate) +- If the session produced no handoff-relevant changes, say so and skip the commit diff --git a/docs/autonomous-session-playbook.md b/docs/autonomous-session-playbook.md index 59c9c7e..2ef1491 100644 --- a/docs/autonomous-session-playbook.md +++ b/docs/autonomous-session-playbook.md @@ -70,7 +70,7 @@ Follow this top to bottom, per ticket. bot's last review rather than waiting indefinitely for a re-review that may not come. Fix real findings (commit + push → re-watch); reply-with-reason to nitpicks you keep. If every configured bot is unavailable (rate-limited, no credits, skipped the PR), run - `review.fallback_command` as the substitute independent pass — a blocked bot is not a + the current runtime's `review.fallback_commands` value as the substitute independent pass — a blocked bot is not a waiver on review. ### Merge + close out diff --git a/docs/friction-log-archive.md b/docs/friction-log-archive.md new file mode 100644 index 0000000..64f5915 --- /dev/null +++ b/docs/friction-log-archive.md @@ -0,0 +1,4 @@ +# Friction Log Archive + +Graduated friction entries live here after they have been routed to the tracker or +promoted into a repeated-pattern rule. diff --git a/docs/getting-started.md b/docs/getting-started.md index 6d1878a..592e926 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,13 +1,16 @@ # Getting started A worked example of one full session with the kit — from adoption through your -first `/wrap-up` — so you can see the basic flow end to end. It assumes you drive -an AI coding agent (Claude Code or similar) that can run these skills and shell +first `wrap-up` — so you can see the basic flow end to end. It assumes you drive +an AI coding agent that can run repository skills and shell commands, on a repo that uses branches + pull requests with review before merge. +Workflow names are runtime-neutral below. Invoke them as `/name` in Claude Code or +`$name` in Codex. + The loop you're setting up: -> **`/session-start` → work → `/pr-watch` → `/wrap-up`**, with the **friction +> **`session-start` → work → `pr-watch` → `wrap-up`**, with the **friction > flywheel** (`/triage-friction-log`, `/post-merge-systemize`) turning underneath. See the [README diagram](../README.md#how-it-fits-together) for the whole picture. @@ -22,7 +25,7 @@ cp -r /path/to/agentic-dev-kit/. . ./init.sh ``` -`init.sh` prompts you for a handful of values — project name, tracker, the +`init.sh` prompts you for a handful of values — project name, agent runtime, tracker, the protected branch, your review bot — and stamps them into `config/dev-model.yaml`. It also seeds `docs/handoff.md` and `docs/friction-log.md` (only if they don't already exist) and adds the state sandbox to `.gitignore`. @@ -31,9 +34,9 @@ Then open `config/dev-model.yaml` and fill in anything you skipped — especiall the `tracker` and `models` blocks. That one file is where every skill and script reads its project-specific values, so there's nothing to hardcode elsewhere. -## 2 · Your first briefing — `/session-start` +## 2 · Your first briefing — `session-start` -Start your agent and run `/session-start`. It reads your handoff, the friction +Start your agent and run `session-start`. It reads your handoff, the friction log, your tracker, open PRs, and CI, then proposes what to do next — grouped by urgency, each candidate tagged `[size · model · mode]`, ending with one pick: @@ -64,7 +67,7 @@ Two modes, by the shape of the task: - **Inline (cockpit)** — anything needing judgment or back-and-forth. You and the agent work in the main session. -- **Isolated lanes** (`/parallel`) — self-contained, *disjoint* tasks run +- **Isolated lanes** (`parallel`) — self-contained, *disjoint* tasks run concurrently, each in its own git worktree with a sandboxed `state/` directory, so parallel agents can't clobber each other's scratch state or the shared plan. Map each lane's file footprint first — the sandbox prevents *state* collisions, @@ -73,13 +76,13 @@ Two modes, by the shape of the task: Reserve the top model tier for the one decision that's expensive to get wrong; let a cheaper tier do the mechanical building. -## 4 · Open a PR and watch it — `/pr-watch` +## 4 · Open a PR and watch it — `pr-watch` Every change goes through a branch and a PR — and opening the PR is *not* the end of the task: ```sh -/pr-watch 42 +pr-watch 42 ``` polls CI and review comments and doesn't stop until the PR is **green and clean**: @@ -88,14 +91,14 @@ reason. A review bot being down isn't a waiver — run an independent review pas instead. For a **risky** change — a send-gate, a destructive migration, a recovery/kill -path — the safety-critical rule (`.claude/rules/safety-critical-changes.md`) +path — the shared safety doctrine (`docs/agentic-dev-kit/safety-critical-changes.md`) raises the bar: prefer a deterministic gate over a fuzzy matcher, use more than one review lens, and require an operator sign-off before merge. Those never self-merge. -## 5 · Close the loop — `/wrap-up` +## 5 · Close the loop — `wrap-up` -At the end of the session, `/wrap-up` updates `docs/handoff.md` with what shipped +At the end of the session, `wrap-up` updates `docs/handoff.md` with what shipped and what's next… ```markdown @@ -117,7 +120,7 @@ and what's next… `tracker.project_id` by hand. Fix: probe for a known config file first. ``` -Because the next `/session-start` reads that handoff, the thread is never lost — +Because the next `session-start` reads that handoff, the thread is never lost — and the friction entry is now queued for the flywheel. ## 6 · Turn the flywheel @@ -141,11 +144,11 @@ and your friction log honest instead of ratcheting every week. ## That's the loop ```text -/session-start → work (inline or /parallel) → /pr-watch → /wrap-up +session-start → work (inline or parallel) → pr-watch → wrap-up │ friction-log ──weekly──► triage + systemize │ - tickets + rules ──► next /session-start + tickets + rules ──► next session-start ``` Each session leaves the repo more legible than it found it: the handoff carries diff --git a/docs/handoff-history.md b/docs/handoff-history.md new file mode 100644 index 0000000..620e059 --- /dev/null +++ b/docs/handoff-history.md @@ -0,0 +1,6 @@ +# Handoff History + +Archived session narratives from the living handoff. Keep active direction and the +next step in `handoff.md`; this file is append-only history. + +## Session log diff --git a/docs/handoff.md b/docs/handoff.md index 22be7e0..6cb8159 100644 --- a/docs/handoff.md +++ b/docs/handoff.md @@ -1,7 +1,7 @@ # my-project — Living Plan (Handoff) > **Forward-looking handoff (Principle #1).** Read this at the start of every session -> (`/session-start`); update it at the end (`/wrap-up`). This file — not an agent's +> (`session-start`); update it at the end (`wrap-up`). This file — not an agent's > memory, not a scratch note — is the single source of truth for what's done, in > progress, and next. > @@ -19,7 +19,7 @@ Last updated: YYYY-MM-DD — - - -▶ Next: +▶ Next: ______________________________________________________________________ diff --git a/docs/parallel-dev.md b/docs/parallel-dev.md index 36a608d..95b3980 100644 --- a/docs/parallel-dev.md +++ b/docs/parallel-dev.md @@ -10,7 +10,9 @@ end-to-end workflow. Engine: [`scripts/dev_session.sh`](../scripts/dev_session.sh) (launch/list/remove lanes) and [`scripts/reconcile_sessions.sh`](../scripts/reconcile_sessions.sh) (drive every lane to a terminal state). The in-session front-end is the -[`/parallel`](../.claude/commands/parallel.md) skill. +shared [`parallel`](agentic-dev-kit/workflows/parallel.md) workflow. These links use +the template default `paths.engines: scripts`; a namespaced adopter substitutes its +configured directory, such as `scripts/devkit`. ## What a lane is @@ -48,7 +50,7 @@ shared module, schema, or config in the same lane and run it sequentially. ```mermaid flowchart TD - CP["cockpit
owns handoff · friction-log · merges"] --> Plan["/parallel plan
cluster candidates by file footprint
· at most one per disjoint cluster"] + CP["cockpit
owns handoff · friction-log · merges"] --> Plan["parallel plan
cluster candidates by file footprint
· at most one per disjoint cluster"] Plan --> L1["lane A
worktree + branch + sandbox"] Plan --> L2["lane B
worktree + branch + sandbox"] Plan --> L3["lane C
worktree + branch + sandbox"] @@ -61,7 +63,7 @@ flowchart TD R --> M["review + merge
self-merge or operator-merge
per lane's pre-assigned class"] ``` -### 1 · Plan the batch — `/parallel plan` +### 1 · Plan the batch — `parallel plan` Don't spin up lanes ticket-by-ticket. Compose the batch deliberately: @@ -84,7 +86,7 @@ Don't spin up lanes ticket-by-ticket. Compose the batch deliberately: ### 2 · Launch each lane — `dev_session.sh new` ```bash -scripts/dev_session.sh new [--base main] [--prefix dev] [--headless] +scripts/dev_session.sh new [--base main] [--prefix dev] [--headless] [--runtime ] [--launcher ] ``` Each `new` creates the worktree + branch + sandbox and prints a copy-paste line that @@ -165,8 +167,8 @@ update `docs/handoff.md` with what shipped — from the cockpit, once. ## See also - [`parallel-howto.md`](parallel-howto.md) — the task-oriented companion: step-by-step - recipes per use case, and *what actually happens* when you run each `/parallel` verb. + recipes per use case, and *what actually happens* when you run each `parallel` verb. - [`PRINCIPLES.md`](../PRINCIPLES.md) #3 (cockpit + isolated lanes), #4 (merge classes), #7 (effort tiering). -- [`.claude/commands/parallel.md`](../.claude/commands/parallel.md) — the full skill. +- [`agentic-dev-kit/workflows/parallel.md`](agentic-dev-kit/workflows/parallel.md) — the shared workflow. - [`docs/getting-started.md`](getting-started.md) — the single-session loop this sits on top of. diff --git a/docs/parallel-howto.md b/docs/parallel-howto.md index e749d10..a094def 100644 --- a/docs/parallel-howto.md +++ b/docs/parallel-howto.md @@ -5,6 +5,10 @@ page explains the **model** — cockpit + isolated lanes, disjoint file footprin it's safe. This one is the **recipe book**: for each thing you actually want to do, the exact commands and *what happens when you run them*. +Examples use the template default `paths.engines: scripts`. If you adopted the kit +under `scripts/devkit`, substitute that configured directory. Invoke the shared +workflow as `/parallel` in Claude Code or `$parallel` in Codex. + If you read only one section, read the next one — it's the point almost everyone trips on the first time. @@ -12,26 +16,26 @@ trips on the first time. Starting a parallel session involves three things that are easy to conflate: -1. **This session** — the agent session you're already in. When it runs a `/parallel` +1. **This session** — the agent session you're already in. When it runs a `parallel` verb it acts as a *launcher / coordinator*. It **does not** turn into the new session, and it **does not** touch your current branch, checkout, or scratch state. 2. **The artifacts on disk** — a git **worktree**, a **branch**, and a **state sandbox** that `dev_session.sh new` creates. Inert until someone opens them. -3. **The new session** — a *separate* agent process (a fresh `claude`) that you start +3. **The new session** — a *separate* process for the configured agent runtime that you start in a **new terminal**. This is the one that actually does work in the new worktree. The single most common surprise: -> **A slash command cannot open the new session for you.** +> **An in-agent workflow cannot open a new interactive terminal for you.** -A `/parallel` skill runs *inside* your current agent process — it can't `cd` your +A `parallel` skill runs *inside* your current agent process — it can't `cd` your terminal into a new directory or spawn a fresh REPL there. So for an interactive lane, `new` does all the setup and then hands you a **copy-paste line**; *you* paste it into a new terminal to actually start the session. (Headless lanes are the exception — see [Use case 4](#use-case-4--launch-an-unattended-headless-lane) — because there is no human terminal to hand the line to.) -So "what is `/parallel new` for, if I still have to open the terminal myself?" — it +So "what is `parallel new` for, if I still have to open the terminal myself?" — it does everything *except* the one step that fundamentally needs your shell: it creates the worktree, branch, and sandbox correctly, and emits the exact line to launch into them. The manual paste is a two-second hand-off, not the work. @@ -42,9 +46,9 @@ Keep that three-way split in mind and every verb below makes sense. | You run | What happens | Touches your current branch? | Read-only? | |---|---|---|---| -| `/parallel`  or  `… list` | Prints the board of active lanes | No | Yes | -| `/parallel list --watch` | Same board, auto-refreshing | No | Yes | -| `/parallel plan` | Proposes a disjoint batch to launch | No | Yes (until you confirm) | +| `parallel`  or  `… list` | Prints the board of active lanes | No | Yes | +| `parallel list --watch` | Same board, auto-refreshing | No | Yes | +| `parallel plan` | Proposes a disjoint batch to launch | No | Yes (until you confirm) | | `dev_session.sh new ` | Creates worktree + branch + sandbox, prints a launch line | No | No — writes to disk, but **not** to your checkout | | *(you paste the launch line)* | Starts the new session in a **new terminal** | No | — | @@ -60,7 +64,7 @@ You just want to know what lanes exist and where they stand. This is the safe, read-only default. ```bash -scripts/dev_session.sh list # or just run /parallel with no argument +scripts/dev_session.sh list # or just run parallel with no argument ``` You get one row per lane: `SCOPE · BRANCH · PR · CI (✓/✗/…) · DIRTY (uncommitted @@ -106,11 +110,11 @@ What this does: 4. Prints a **copy-paste line** and stops: ```text - cd && export DEVKIT_STATE_ROOT= && export DEVKIT_ROOT= && claude + cd && export DEVKIT_STATE_ROOT= && export DEVKIT_ROOT= && ``` -**Then you** open a new terminal and paste that line. That — not the slash command — -is what starts the new `claude`. Your current session keeps running, unchanged, on its +**Then you** open a new terminal and paste that line. That — not the workflow — +is what starts the new agent process. Your current session keeps running, unchanged, on its own branch. Useful options: @@ -120,6 +124,9 @@ Useful options: current one). - `--prefix

` — branch namespace (default `dev`, giving `dev/`). - `--branch ` — override the whole branch name. +- `--runtime ` — select a key from `runtime.launchers` for this lane. +- `--launcher ` — override the configured command for this lane; use + `--launcher none` to print activation-only guidance. > **Do I even need `new` for a throwaway?** If you only want to read or experiment with > **no** branch and **no** state isolation, a plain `git worktree add ../scratch main` @@ -135,7 +142,7 @@ the batch so no two lanes edit the same source file (the sandbox prevents *state collisions, not *source* merge conflicts). Start from the planner: ```text -/parallel plan # or: /parallel plan +parallel plan # or: parallel plan ``` It gathers candidate work, **clusters it by file footprint**, drops stale-premise @@ -219,13 +226,13 @@ worktree (`new`). Your current session stays on its own branch the whole time. No — off `origin/main` by default, so it starts clean. Pass `--base ` if you specifically want it based on something else (including your current branch). -**Why can't `/parallel new` just open the new session for me?** -Because a slash command runs inside your current agent process. It can't move your -terminal into a new directory or start a fresh `claude` there — that needs your shell. +**Why can't `parallel new` just open the new session for me?** +Because an in-agent workflow runs inside your current agent process. It can't move your +terminal into a new directory or start a fresh agent process there — that needs your shell. So it does everything up to that point and hands you the launch line. Headless lanes skip the hand-off because there's no terminal involved. -**Is running `/parallel` (no argument) ever destructive?** +**Is running `parallel` (no argument) ever destructive?** No. With no argument it just prints the board and stops. It's the `git status` of your lane fleet. @@ -233,6 +240,6 @@ lane fleet. - [`parallel-dev.md`](parallel-dev.md) — the concept: cockpit + isolated lanes, disjoint footprints, the batch workflow, the worked example. -- [`.claude/commands/parallel.md`](../.claude/commands/parallel.md) — the full skill. +- [`agentic-dev-kit/workflows/parallel.md`](agentic-dev-kit/workflows/parallel.md) — the shared workflow. - [`PRINCIPLES.md`](../PRINCIPLES.md) #3 (cockpit + isolated lanes), #7 (effort tiering). diff --git a/init.sh b/init.sh index a20d2da..8ff3021 100644 --- a/init.sh +++ b/init.sh @@ -18,7 +18,7 @@ Usage: ./init.sh [--help] Bootstraps the agentic-dev-kit in the current repo: - 1. Prompts for project.name, tracker.backend (+ project name and, for + 1. Prompts for project.name, runtime.default, tracker.backend (+ project name and, for Linear, team/project ids), vcs.protected_branch, notify.user_key, and review.bots — each showing the current value in config/dev-model.yaml as the default. Press Enter to keep the default. @@ -27,7 +27,7 @@ Bootstraps the agentic-dev-kit in the current repo: skeletons, but ONLY if those files don't already exist. 4. Appends the kit's state-sandbox paths to .gitignore if they're missing (never duplicates a line on re-run). - 5. Prints the next step: run /session-start. + 5. Prints the runtime-specific session-start invocation. Safe to re-run at any time. Run it from the repo root (the directory that contains config/dev-model.yaml). @@ -166,6 +166,10 @@ cur_name=$(get_field "project:" "" "^ name:") name=$(ask "Project name" "$cur_name") set_field "project:" "" "^ name:" "$name" +cur_runtime=$(get_field "runtime:" "" "^ default:") +runtime=$(ask "Agent runtime (claude | codex | none)" "$cur_runtime") +set_field "runtime:" "" "^ default:" "$runtime" + cur_backend=$(get_field "tracker:" "" "^ backend:") backend=$(ask "Tracker backend (linear | github-issues | jira | none)" "$cur_backend") set_field "tracker:" "" "^ backend:" "$backend" @@ -212,7 +216,7 @@ if [ ! -f docs/handoff.md ]; then # ${name} — Living Plan (Handoff) > **Forward-looking handoff (Principle #1).** Read this at the start of every session -> (\`/session-start\`); update it at the end (\`/wrap-up\`). This file — not an agent's +> (\`session-start\`); update it at the end (\`wrap-up\`). This file — not an agent's > memory, not a scratch note — is the single source of truth for what's done, in > progress, and next. > @@ -230,7 +234,7 @@ Last updated: YYYY-MM-DD — - - -▶ Next: +▶ Next: ______________________________________________________________________ @@ -288,6 +292,10 @@ add_ignore_line ".devkit_state_root" echo "" echo "agentic-dev-kit is bootstrapped." echo "Review config/dev-model.yaml for any remaining values (paths, doc_budgets," -echo "models, tracker.url, review.fallback_command) and edit to taste." +echo "models, tracker.url, review.fallback_commands) and edit to taste." echo "" -echo "You're set — run /session-start next." +case "$runtime" in + codex) echo "You're set — invoke \$session-start next." ;; + claude) echo "You're set — run /session-start next." ;; + *) echo "You're set — invoke the session-start workflow in your agent runtime next." ;; +esac diff --git a/scripts/archive_plan_sessions.py b/scripts/archive_plan_sessions.py index e31266e..350cfc8 100755 --- a/scripts/archive_plan_sessions.py +++ b/scripts/archive_plan_sessions.py @@ -1,7 +1,7 @@ -#!/usr/bin/env python3 +#!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = [] +# dependencies = ["pyyaml"] # /// """Sweep old session blocks out of ``docs/handoff.md`` into ``docs/handoff-history.md``. @@ -23,10 +23,10 @@ Usage: - python3 scripts/archive_plan_sessions.py # keep 6, apply - python3 scripts/archive_plan_sessions.py --keep 5 - python3 scripts/archive_plan_sessions.py --dry-run # report only - python3 scripts/archive_plan_sessions.py --plan docs/handoff.md --history docs/handoff-history.md + uv run scripts/archive_plan_sessions.py # keep 6, apply + uv run scripts/archive_plan_sessions.py --keep 5 + uv run scripts/archive_plan_sessions.py --dry-run # report only + uv run scripts/archive_plan_sessions.py --plan docs/handoff.md --history docs/handoff-history.md Exit codes: 0 — applied (or nothing to do, or dry-run) @@ -40,17 +40,10 @@ import sys from pathlib import Path -def _find_repo_root(start: Path) -> Path: - """Nearest ancestor with a ``.git`` marker (so this keeps working when the kit - is vendored under a nested dir, e.g. scripts/devkit/); falls back to the - script's grandparent if no marker is found.""" - for candidate in (start, *start.parents): - if (candidate / ".git").exists(): - return candidate - return start.parent.parent +sys.path.insert(0, str(Path(__file__).resolve().parent / "lib")) +from devmodel_config import _repo_root, load_config, resolve_path # noqa: E402 - -REPO_ROOT = _find_repo_root(Path(__file__).resolve()) +REPO_ROOT = _repo_root() SEP = "______________________________________________________________________\n" SESSION_PREFIXES = ("## Latest session", "## Earlier session", "## Session — ") # Recent sessions may write *dated* headings (`## June 5 Fri (cont.) — …`) or a @@ -77,6 +70,17 @@ def _find_repo_root(start: Path) -> Path: ] +def configured_paths( + root: Path = REPO_ROOT, config_path: Path | None = None +) -> tuple[Path, Path]: + """Resolve the live handoff and history paths from ``dev-model.yaml``.""" + config = load_config(config_path or root / "config" / "dev-model.yaml") + return ( + resolve_path(config, "paths.handoff", root=root), + resolve_path(config, "paths.handoff_history", root=root), + ) + + def _is_session_heading(line: str) -> bool: return line.startswith(SESSION_PREFIXES) or bool(_DATED_SESSION_RE.match(line)) @@ -191,11 +195,12 @@ def main(argv: list[str] | None = None) -> int: Returns 0 on success / no-op / dry-run, 2 on usage error, unparseable handoff structure, or a failed write (the handoff doc is rolled back in that case). """ + default_plan, default_history = configured_paths() parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--keep", type=int, default=DEFAULT_KEEP, help="live blocks to keep") - parser.add_argument("--plan", type=Path, default=REPO_ROOT / "docs/handoff.md", help="living handoff doc") + parser.add_argument("--plan", type=Path, default=default_plan, help="living handoff doc") parser.add_argument( - "--history", type=Path, default=REPO_ROOT / "docs/handoff-history.md", help="handoff history/archive doc" + "--history", type=Path, default=default_history, help="handoff history/archive doc" ) parser.add_argument("--dry-run", action="store_true", help="report only, write nothing") args = parser.parse_args(argv) diff --git a/scripts/dev_session.sh b/scripts/dev_session.sh index 59eda2c..4fec220 100755 --- a/scripts/dev_session.sh +++ b/scripts/dev_session.sh @@ -13,7 +13,7 @@ # runs in zero sessions until something sets it. `new` is what finally sets it. # # Usage: -# scripts/dev_session.sh new [--base main] [--prefix dev] [--branch ] [--force] [--headless] +# scripts/dev_session.sh new [--base main] [--prefix dev] [--branch ] [--force] [--headless] [--runtime ] [--launcher ] # scripts/dev_session.sh list [--watch [interval]] # scripts/dev_session.sh path # scripts/dev_session.sh rm [--force] [--keep-branch] @@ -26,11 +26,12 @@ # before. # # `new` prints a copy-paste line that cd's into the worktree, exports the -# sandbox, and launches claude — or `source /activate` in any shell. +# sandbox, and prints a runtime-configured launch command — or `source +# /activate` in any shell. # # `new --headless` instead writes a sticky `/.devkit_state_root` # marker (the sandbox path) and prints a JSON descriptor to stdout — so an -# unattended launcher (background Agent / Claude web) can point an agent at the +# unattended launcher (background agent / cloud runtime) can point an agent at the # worktree and its state/ writes isolate via the marker, no env export needed. # Diagnostics go to stderr in this mode so stdout is clean JSON. The # descriptor also carries `prompt_preamble` — the canonical lane-contract text @@ -44,7 +45,7 @@ # # `print-contract` prints ONLY the lane-contract text (no JSON, no worktree # side effects) — for a launcher that wants the raw preamble once and reuses it -# across many lanes (e.g. a `/parallel`-style Workflow fan-out path), or for +# across many lanes (e.g. a parallel-workflow fan-out path), or for # eyeballing the current contract. # # Sessions live outside the repo (a sibling `dev-model-sessions/` dir by @@ -54,7 +55,13 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +# shellcheck source=scripts/lib/repo_root.sh +source "$SCRIPT_DIR/lib/repo_root.sh" +REPO_ROOT="$(devkit_find_repo_root "$SCRIPT_DIR")" || { + echo "[dev-session] error: no .git repository found above $SCRIPT_DIR" >&2 + exit 1 +} +ENGINE_DIR_REL="${SCRIPT_DIR#"$REPO_ROOT"/}" # Sessions container (sibling of the repo by default). Each session is one # subdir holding wt/ (worktree) + state/ (sandbox) + activate (env snippet). @@ -90,16 +97,29 @@ HEADLESS=0 # bind a freshly spawned lane. Keep this the SINGLE source of the contract # text — any doc that quotes it should quote it, not restate it independently. _lane_contract() { - cat <<'CONTRACT' + local handoff friction protected + local escaped_engine_dir escaped_handoff escaped_friction escaped_protected + handoff="$(_config_scalar paths "" handoff)" + friction="$(_config_scalar paths "" friction_log)" + protected="$(_config_scalar vcs "" protected_branch)" + escaped_engine_dir="$(_sed_replacement "$ENGINE_DIR_REL")" + escaped_handoff="$(_sed_replacement "${handoff:-}")" + escaped_friction="$(_sed_replacement "${friction:-}")" + escaped_protected="$(_sed_replacement "${protected:-the protected branch}")" + sed \ + -e "s|@ENGINE_DIR@|$escaped_engine_dir|g" \ + -e "s|@HANDOFF@|$escaped_handoff|g" \ + -e "s|@FRICTION_LOG@|$escaped_friction|g" \ + -e "s|@PROTECTED_BRANCH@|$escaped_protected|g" <<'CONTRACT' LANE CONTRACT (binding): - Actively poll your PR's CI at a bounded cadence (e.g. `gh pr checks ` every few minutes, capped around 30 min) until it is fully green. Never stop to idly wait on a "monitor", a timer, or someone else's watcher — you are the one polling. - Your run ends ONLY at the terminal state. Never stop early to wait for a watcher, monitor, or timer of any kind — if you need to wait on anything, poll it yourself with a bounded until-loop. - Stop at draft-PR-green and hand off. Open your PR as a DRAFT on first push and leave it in draft. Do not mark it ready, do not merge — the cockpit owns ready-for-review, the review pass, and the terminal merge. -- `gh`'s draft bit is flaky: after `gh pr create --draft`, run `uv run scripts/pr_watch.py --assert-draft` — a create that silently lands non-draft triggers premature bot review. -- Report every finding, decision, and open question in your FINAL TEXT response — that is the only channel back to the cockpit. Never rely on a peer SendMessage to another agent to deliver results. -- If you spawn sub-agents of your own, hold them to the same rule: they return findings in their final text, never via SendMessage to a peer — peer messaging by type-name is unreachable from a spawned sub-agent. -- Never edit docs/handoff.md or docs/friction-log.md — those are cockpit-owned shared narrative files. Put your handoff (what shipped, lessons, deferrals) in the PR body instead. -- Run `git branch --show-current` before every commit to confirm you are on your own lane branch, never main. +- `gh`'s draft bit is flaky: after `gh pr create --draft`, run `uv run @ENGINE_DIR@/pr_watch.py --assert-draft` — a create that silently lands non-draft triggers premature bot review. +- Report every finding, decision, and open question in your FINAL TEXT response — that is the durable channel back to the cockpit. Never rely exclusively on a runtime-specific peer-message mechanism. +- If you spawn sub-agents of your own, hold them to the same rule: they return findings in their final text rather than relying exclusively on peer messages. +- Never edit @HANDOFF@ or @FRICTION_LOG@ — those are cockpit-owned shared narrative files. Put your handoff (what shipped, lessons, deferrals) in the PR body instead. +- Run `git branch --show-current` before every commit to confirm you are on your own lane branch, never @PROTECTED_BRANCH@. CONTRACT } @@ -136,6 +156,57 @@ _slug_ok() { [[ "$1" =~ ^[a-z0-9][a-z0-9-]*$ ]] } +# Read one scalar from the kit's deliberately simple, hand-authored YAML config. +# This avoids adding a YAML dependency to the shell launcher. It supports exactly +# the top-level/subsection/scalar shape used by runtime.default and +# runtime.launchers.. +_config_scalar() { + local section="$1" subsection="$2" key="$3" + [[ -n "$subsection" ]] && subsection="$subsection:" + awk -v want_section="$section:" -v want_subsection="$subsection" -v want_key="$key:" ' + function trim(s) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", s); return s } + /^[A-Za-z_][A-Za-z0-9_]*:[[:space:]]*$/ { + current_section = trim($0); current_subsection = ""; next + } + /^ [A-Za-z_][A-Za-z0-9_]*:[[:space:]]*$/ { + current_subsection = trim($0); next + } + current_section == want_section && current_subsection == want_subsection { + line = $0 + stripped = line + sub(/^[[:space:]]+/, "", stripped) + if (index(stripped, want_key) == 1) { + value = substr(stripped, length(want_key) + 1) + sub(/[[:space:]]+#.*/, "", value) + value = trim(value) + gsub(/^"|"$/, "", value) + print value + exit + } + } + ' "$REPO_ROOT/config/dev-model.yaml" 2>/dev/null +} + +_sed_replacement() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//&/\\&}" + value="${value//|/\\|}" + printf '%s' "$value" +} + +_resolve_launcher() { + local requested_runtime="$1" requested_launcher="$2" + local runtime launcher + runtime="${requested_runtime:-${DEVKIT_RUNTIME:-$(_config_scalar runtime "" default)}}" + launcher="${requested_launcher:-${DEVKIT_AGENT_LAUNCHER:-}}" + if [[ -z "$launcher" && -n "$runtime" && "$runtime" != "none" ]]; then + launcher="$(_config_scalar runtime launchers "$runtime")" + fi + [[ "$launcher" == "none" ]] && launcher="" + printf '%s\t%s\n' "${runtime:-none}" "$launcher" +} + # True when $1 is a branch `rm` must never delete, regardless of merged status: # the session's base branch, or the universal mainlines main/master. The last # line of defense against a worktree HEAD that wandered onto main after a @@ -151,6 +222,7 @@ _is_protected_branch() { cmd_new() { local scope="" base="main" prefix="$DEFAULT_PREFIX" branch="" force=0 + local requested_runtime="" requested_launcher="" runtime="" launcher="" HEADLESS=0 while [[ $# -gt 0 ]]; do case "$1" in @@ -159,13 +231,16 @@ cmd_new() { --branch) [[ $# -ge 2 ]] || _die "--branch needs a value"; branch="$2"; shift 2 ;; --force) force=1; shift ;; --headless) HEADLESS=1; shift ;; + --runtime) [[ $# -ge 2 ]] || _die "--runtime needs a value"; requested_runtime="$2"; shift 2 ;; + --launcher) [[ $# -ge 2 ]] || _die "--launcher needs a value"; requested_launcher="$2"; shift 2 ;; -*) _die "unknown flag: $1" ;; *) [[ -z "$scope" ]] && scope="$1" && shift || _die "unexpected arg: $1" ;; esac done - [[ -n "$scope" ]] || _die "usage: dev_session.sh new [--base main] [--prefix dev] [--branch ] [--force] [--headless]" + [[ -n "$scope" ]] || _die "usage: dev_session.sh new [--base main] [--prefix dev] [--branch ] [--force] [--headless] [--runtime ] [--launcher ]" _slug_ok "$scope" || _die "scope must be a lowercase slug ([a-z0-9-]): got '$scope'" [[ -z "$branch" ]] && branch="${prefix}/${scope}" + IFS=$'\t' read -r runtime launcher <<< "$(_resolve_launcher "$requested_runtime" "$requested_launcher")" # Refuse a protected branch as the session branch BEFORE any side effect. `--branch` # is unvalidated, so `new x --branch main --force` would otherwise tear the existing @@ -286,11 +361,11 @@ ACTIVATE # is the map of env vars the launcher MUST export into the lane's # process — currently just the fail-closed sandbox guard. python3 - "$scope" "$branch" "$worktree_abs" "$sandbox_abs" "$repo_root_abs" "$base" \ - "$(_lane_contract)" "$REFUSE_UNSANDBOXED_ENV_VALUE" >&3 <<'PY' + "$(_lane_contract)" "$REFUSE_UNSANDBOXED_ENV_VALUE" "$runtime" "$launcher" >&3 <<'PY' import json import sys -scope, branch, worktree, state_root, repo_root, base, prompt_preamble, refuse_unsandboxed = sys.argv[1:9] +scope, branch, worktree, state_root, repo_root, base, prompt_preamble, refuse_unsandboxed, runtime, launcher = sys.argv[1:11] print( json.dumps( { @@ -302,6 +377,8 @@ print( "base": base, "prompt_preamble": prompt_preamble, "env": {"DEVKIT_REFUSE_UNSANDBOXED_STATE": refuse_unsandboxed}, + "runtime": runtime, + "launcher": launcher or None, } ) ) @@ -312,11 +389,16 @@ PY echo echo "✓ session '$scope' ready." echo - echo " Start it (copy-paste):" - echo " cd \"$worktree\" && export DEVKIT_STATE_ROOT=\"$sandbox\" && export DEVKIT_ROOT=\"$REPO_ROOT\" && claude" + if [[ -n "$launcher" ]]; then + echo " Start it with $runtime (copy-paste):" + echo " cd \"$worktree\" && export DEVKIT_STATE_ROOT=\"$sandbox\" && export DEVKIT_ROOT=\"$REPO_ROOT\" && $launcher" + else + echo " Activate it, then start your agent runtime:" + echo " source \"$session_dir/activate\"" + fi echo echo " …or in any shell: source \"$session_dir/activate\"" - echo " When done: scripts/dev_session.sh rm $scope" + echo " When done: \"${BASH_SOURCE[0]}\" rm $scope" } # Gather one TAB-separated record per active session, the shared data behind both @@ -344,7 +426,7 @@ _collect_board() { pr_ok=1 # CI rollup → one glyph. The failing/pending vocabularies mirror # scripts/pr_watch.py:summarize_checks (the other cockpit CI surface) so - # the board and /pr-watch agree — notably TIMED_OUT/ACTION_REQUIRED render + # the board and pr-watch agree — notably TIMED_OUT/ACTION_REQUIRED render # ✗, not a misleading ✓, and EXPECTED/QUEUED render … (pending). parsed="$(printf '%s' "$pr_json" | python3 -c ' import sys, json @@ -559,7 +641,7 @@ cmd_path() { # Print ONLY the canonical lane-contract text (no JSON, no side effects) — for # a launcher that wants the raw preamble once and reuses it across every lane -# in a batch (e.g. a /parallel-style Workflow fan-out path), rather than +# in a batch (e.g. a parallel-workflow fan-out path), rather than # parsing it back out of each --headless JSON descriptor. cmd_print_contract() { _lane_contract diff --git a/scripts/lib/repo_root.sh b/scripts/lib/repo_root.sh new file mode 100644 index 0000000..563c47c --- /dev/null +++ b/scripts/lib/repo_root.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Shared repository-root discovery for shell engines. + +devkit_find_repo_root() { + local candidate + candidate="$(cd "$1" && pwd -P)" || return 1 + while [[ "$candidate" != "/" ]]; do + if [[ -e "$candidate/.git" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + candidate="$(dirname "$candidate")" + done + return 1 +} diff --git a/scripts/pr_watch.py b/scripts/pr_watch.py index de1dd5e..e2da28e 100755 --- a/scripts/pr_watch.py +++ b/scripts/pr_watch.py @@ -5,7 +5,8 @@ # /// """Poll a PR's CI checks + new review comments — the engine for the watch-and-fix loop. -`/pr-watch` (and the "PR follow-through" policy in your project's CLAUDE.md) +The shared `pr-watch` workflow (and the "PR follow-through" policy in your project's +agent instructions) call this once per round: it asks `gh` for the PR's check rollup + every comment surface (issue comments, review submissions, inline review comments), filters out known auto-noise (a review bot's billing notice, its walkthrough / diff --git a/scripts/reconcile_sessions.sh b/scripts/reconcile_sessions.sh index c758120..2129539 100755 --- a/scripts/reconcile_sessions.sh +++ b/scripts/reconcile_sessions.sh @@ -48,7 +48,12 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +# shellcheck source=scripts/lib/repo_root.sh +source "$SCRIPT_DIR/lib/repo_root.sh" +REPO_ROOT="$(devkit_find_repo_root "$SCRIPT_DIR")" || { + echo "[reconcile] error: no .git repository found above $SCRIPT_DIR" >&2 + exit 64 +} # Sessions container — mirror dev_session.sh so no-arg discovery lines up with # the sibling that created the sessions. diff --git a/scripts/tests/test_portability.py b/scripts/tests/test_portability.py new file mode 100644 index 0000000..b04d42e --- /dev/null +++ b/scripts/tests/test_portability.py @@ -0,0 +1,162 @@ +from __future__ import annotations + +import importlib.util +import shutil +import subprocess +from pathlib import Path +from types import ModuleType + +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def _load_module(name: str, path: Path) -> ModuleType: + spec = importlib.util.spec_from_file_location(name, path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _install_nested_shell_engines(repo: Path) -> Path: + engine_dir = repo / "scripts" / "devkit" + (engine_dir / "lib").mkdir(parents=True) + shutil.copy2( + REPO_ROOT / "scripts" / "dev_session.sh", engine_dir / "dev_session.sh" + ) + shutil.copy2( + REPO_ROOT / "scripts" / "reconcile_sessions.sh", + engine_dir / "reconcile_sessions.sh", + ) + shutil.copy2( + REPO_ROOT / "scripts" / "lib" / "repo_root.sh", + engine_dir / "lib" / "repo_root.sh", + ) + (repo / ".git").mkdir() + (repo / "config").mkdir() + (repo / "config" / "dev-model.yaml").write_text( + """paths: + handoff: handoff.md + friction_log: friction-log.md +runtime: + default: codex + launchers: + claude: claude + codex: codex +vcs: + protected_branch: trunk +""", + encoding="utf-8", + ) + return engine_dir + + +def test_nested_shell_engines_find_the_repository_root(tmp_path: Path) -> None: + repo = tmp_path / "project" + engine_dir = _install_nested_shell_engines(repo) + + result = subprocess.run( + ["bash", str(engine_dir / "dev_session.sh"), "list"], + check=True, + capture_output=True, + text=True, + ) + + expected_sessions = repo.parent / "dev-model-sessions" + assert f"no sessions — {expected_sessions} does not exist yet" in result.stdout + + +def test_nested_launcher_resolves_runtime_mapping(tmp_path: Path) -> None: + repo = tmp_path / "project" + engine_dir = _install_nested_shell_engines(repo) + script = engine_dir / "dev_session.sh" + + result = subprocess.run( + ["bash", "-c", 'source "$1"; _resolve_launcher "" ""', "bash", str(script)], + check=True, + capture_output=True, + text=True, + ) + + assert result.stdout == "codex\tcodex\n" + + +def test_nested_lane_contract_uses_configured_paths(tmp_path: Path) -> None: + repo = tmp_path / "project" + engine_dir = _install_nested_shell_engines(repo) + script = engine_dir / "dev_session.sh" + + result = subprocess.run( + ["bash", "-c", 'source "$1"; _lane_contract', "bash", str(script)], + check=True, + capture_output=True, + text=True, + ) + + assert "scripts/devkit/pr_watch.py" in result.stdout + assert "Never edit handoff.md or friction-log.md" in result.stdout + assert "never trunk" in result.stdout + + +def test_archive_defaults_follow_configured_paths(tmp_path: Path) -> None: + repo = tmp_path / "project" + config_path = repo / "config" / "dev-model.yaml" + config_path.parent.mkdir(parents=True) + config_path.write_text( + """paths: + handoff: handoff.md + handoff_history: saved/handoff-history.md +""", + encoding="utf-8", + ) + archive = _load_module( + "archive_plan_sessions", REPO_ROOT / "scripts" / "archive_plan_sessions.py" + ) + + plan, history = archive.configured_paths(root=repo, config_path=config_path) + + assert plan == repo / "handoff.md" + assert history == repo / "saved" / "handoff-history.md" + + +def test_python_engine_root_walk_supports_namespacing(tmp_path: Path) -> None: + repo = tmp_path / "project" + nested_script = repo / "scripts" / "devkit" / "pr_watch.py" + nested_script.parent.mkdir(parents=True) + (repo / ".git").mkdir() + pr_watch = _load_module("pr_watch", REPO_ROOT / "scripts" / "pr_watch.py") + + assert pr_watch._find_repo_root(nested_script) == repo + + +def test_codex_skill_adapters_are_valid_and_share_workflows() -> None: + for name in ("session-start", "wrap-up", "pr-watch", "parallel"): + skill_dir = REPO_ROOT / ".agents" / "skills" / name + skill_text = (skill_dir / "SKILL.md").read_text(encoding="utf-8") + assert skill_text.startswith("---\n") + _, frontmatter, body = skill_text.split("---", 2) + metadata = yaml.safe_load(frontmatter) + assert set(metadata) == {"name", "description"} + assert metadata["name"] == name + assert "TODO" not in skill_text + assert f"docs/agentic-dev-kit/workflows/{name}.md" in body + + interface = yaml.safe_load( + (skill_dir / "agents" / "openai.yaml").read_text(encoding="utf-8") + )["interface"] + assert 25 <= len(interface["short_description"]) <= 64 + assert f"${name}" in interface["default_prompt"] + + claude_adapter = (REPO_ROOT / ".claude" / "commands" / f"{name}.md").read_text( + encoding="utf-8" + ) + assert f"docs/agentic-dev-kit/workflows/{name}.md" in claude_adapter + + +def test_shared_lane_contract_has_no_runtime_specific_peer_api() -> None: + script = (REPO_ROOT / "scripts" / "dev_session.sh").read_text(encoding="utf-8") + + assert "SendMessage" not in script + assert "&& claude" not in script From f57299a7f0f1957b33d131ae50a36f8c367289ac Mon Sep 17 00:00:00 2001 From: Topi Jarvinen Date: Wed, 15 Jul 2026 12:33:03 +0300 Subject: [PATCH 2/3] test: keep portability suite namespace-safe --- scripts/tests/test_portability.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/scripts/tests/test_portability.py b/scripts/tests/test_portability.py index b04d42e..15bf3e7 100644 --- a/scripts/tests/test_portability.py +++ b/scripts/tests/test_portability.py @@ -9,7 +9,17 @@ import yaml -REPO_ROOT = Path(__file__).resolve().parents[2] +ENGINE_DIR = Path(__file__).resolve().parent.parent + + +def _find_repo_root(start: Path) -> Path: + for candidate in (start, *start.parents): + if (candidate / ".git").exists(): + return candidate + raise RuntimeError(f"no repository root above {start}") + + +REPO_ROOT = _find_repo_root(ENGINE_DIR) def _load_module(name: str, path: Path) -> ModuleType: @@ -24,14 +34,14 @@ def _install_nested_shell_engines(repo: Path) -> Path: engine_dir = repo / "scripts" / "devkit" (engine_dir / "lib").mkdir(parents=True) shutil.copy2( - REPO_ROOT / "scripts" / "dev_session.sh", engine_dir / "dev_session.sh" + ENGINE_DIR / "dev_session.sh", engine_dir / "dev_session.sh" ) shutil.copy2( - REPO_ROOT / "scripts" / "reconcile_sessions.sh", + ENGINE_DIR / "reconcile_sessions.sh", engine_dir / "reconcile_sessions.sh", ) shutil.copy2( - REPO_ROOT / "scripts" / "lib" / "repo_root.sh", + ENGINE_DIR / "lib" / "repo_root.sh", engine_dir / "lib" / "repo_root.sh", ) (repo / ".git").mkdir() @@ -112,7 +122,7 @@ def test_archive_defaults_follow_configured_paths(tmp_path: Path) -> None: encoding="utf-8", ) archive = _load_module( - "archive_plan_sessions", REPO_ROOT / "scripts" / "archive_plan_sessions.py" + "archive_plan_sessions", ENGINE_DIR / "archive_plan_sessions.py" ) plan, history = archive.configured_paths(root=repo, config_path=config_path) @@ -126,7 +136,7 @@ def test_python_engine_root_walk_supports_namespacing(tmp_path: Path) -> None: nested_script = repo / "scripts" / "devkit" / "pr_watch.py" nested_script.parent.mkdir(parents=True) (repo / ".git").mkdir() - pr_watch = _load_module("pr_watch", REPO_ROOT / "scripts" / "pr_watch.py") + pr_watch = _load_module("pr_watch", ENGINE_DIR / "pr_watch.py") assert pr_watch._find_repo_root(nested_script) == repo @@ -156,7 +166,7 @@ def test_codex_skill_adapters_are_valid_and_share_workflows() -> None: def test_shared_lane_contract_has_no_runtime_specific_peer_api() -> None: - script = (REPO_ROOT / "scripts" / "dev_session.sh").read_text(encoding="utf-8") + script = (ENGINE_DIR / "dev_session.sh").read_text(encoding="utf-8") assert "SendMessage" not in script assert "&& claude" not in script From 30fbd930d7beb21089e0525f9314c9eff6d4849c Mon Sep 17 00:00:00 2001 From: Topi Jarvinen Date: Wed, 15 Jul 2026 12:40:26 +0300 Subject: [PATCH 3/3] fix: support recent-sessions handoffs --- scripts/archive_plan_sessions.py | 80 +++++++++++++++++++++++++------ scripts/tests/test_portability.py | 67 ++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 15 deletions(-) diff --git a/scripts/archive_plan_sessions.py b/scripts/archive_plan_sessions.py index 350cfc8..900d404 100755 --- a/scripts/archive_plan_sessions.py +++ b/scripts/archive_plan_sessions.py @@ -3,12 +3,13 @@ # requires-python = ">=3.12" # dependencies = ["pyyaml"] # /// -"""Sweep old session blocks out of ``docs/handoff.md`` into ``docs/handoff-history.md``. +"""Sweep old session blocks out of the live handoff into its history document. The living handoff doc keeps only the most-recent handful of session blocks — -whether written as ``## Latest session`` / ``## Earlier session`` or as a bare -dated ``## June 5 Fri (cont.) — …`` heading; everything older belongs in the -append-only ``handoff-history.md`` Session log. Doing this by hand is +whether written as ``## Latest session`` / ``## Earlier session``, as a bare +dated ``## June 5 Fri (cont.) — …`` heading, or as ``###`` entries below a +``## Recent sessions`` section; everything older belongs in the append-only +handoff history. Doing this by hand is error-prone (an unswept handoff doc balloons over time, making every session start more expensive), so this script makes it a **deterministic, idempotent** operation: keep the newest ``--keep`` blocks live, move the rest verbatim into @@ -60,6 +61,8 @@ ) _DATED_SESSION_RE = re.compile(rf"^## (?:{_MONTHS}) \d{{1,2}}\b") DEFAULT_KEEP = 6 +RECENT_SESSIONS_HEADING = "## Recent sessions" +HISTORY_SECTION_HEADINGS = ("## Session log", "## Recent sessions (archived)") POINTER = [ "> Older session entries (below the live blocks above) live in [`handoff-history.md`](handoff-history.md).\n", @@ -86,7 +89,8 @@ def _is_session_heading(line: str) -> bool: def _is_sep(line: str) -> bool: - return line.strip().startswith("_") and set(line.strip()) == {"_"} + stripped = line.strip() + return len(stripped) >= 3 and set(stripped) in ({"_"}, {"-"}) def split_plan(lines: list[str]) -> tuple[list[str], list[str], list[str]]: @@ -96,15 +100,43 @@ def split_plan(lines: list[str]) -> tuple[list[str], list[str], list[str]]: session blocks plus any inter-block separators and the existing pointer; ``tail`` is the first non-session ``##`` heading (standing sections) onward. """ - try: - sess_start = next(i for i, ln in enumerate(lines) if _is_session_heading(ln)) - except StopIteration as exc: # pragma: no cover - defensive - raise ValueError("no '## Latest/Earlier session' block found in handoff doc") from exc + sess_start = next((i for i, ln in enumerate(lines) if _is_session_heading(ln)), None) + if sess_start is not None: + standing = next( + ( + i + for i, ln in enumerate(lines) + if i > sess_start + and ln.startswith("## ") + and not _is_session_heading(ln) + ), + len(lines), + ) + return lines[:sess_start], lines[sess_start:standing], lines[standing:] + + recent_start = next( + ( + i + for i, ln in enumerate(lines) + if ln.rstrip("\n") == RECENT_SESSIONS_HEADING + ), + None, + ) + if recent_start is None: + raise ValueError( + "no session blocks or '## Recent sessions' section found in handoff doc" + ) standing = next( - (i for i, ln in enumerate(lines) if i > sess_start and ln.startswith("## ") and not _is_session_heading(ln)), + ( + i + for i, ln in enumerate(lines) + if i > recent_start and ln.startswith("## ") + ), len(lines), ) - return lines[:sess_start], lines[sess_start:standing], lines[standing:] + # Keep the section heading in the head; parse_blocks handles its ``###`` + # entries and rebuild_plan preserves this layout without adding a pointer. + return lines[: recent_start + 1], lines[recent_start + 1 : standing], lines[standing:] def parse_blocks(region: list[str]) -> list[list[str]]: @@ -114,9 +146,16 @@ def parse_blocks(region: list[str]) -> list[list[str]]: so the pointer the previous run wrote never gets absorbed into a block. """ blocks: list[list[str]] = [] + uses_recent_sections = not any(_is_session_heading(line) for line in region) + + def is_block_heading(line: str) -> bool: + if uses_recent_sections: + return line.startswith("### ") + return _is_session_heading(line) + cur: list[str] | None = None for line in region: - if _is_session_heading(line): + if is_block_heading(line): if cur is not None: blocks.append(cur) cur = [line] @@ -165,6 +204,12 @@ def trim_megaline(head: list[str], keep: int) -> list[str]: def rebuild_plan(head: list[str], keep_blocks: list[list[str]], tail: list[str], keep: int) -> list[str]: """Reassemble the handoff doc from the trimmed head, kept blocks, fresh pointer, and tail.""" + if head and head[-1].rstrip("\n") == RECENT_SESSIONS_HEADING: + body: list[str] = ["\n"] + for block in keep_blocks: + body += block + ["\n", "---\n", "\n"] + return head + body + tail + head = trim_megaline(head, keep) body: list[str] = [] for block in keep_blocks: @@ -174,11 +219,16 @@ def rebuild_plan(head: list[str], keep_blocks: list[list[str]], tail: list[str], def insert_into_history(history: list[str], moved: list[list[str]]) -> list[str]: - """Insert demoted blocks at the top of the history doc's Session log.""" + """Insert demoted blocks at the top of a recognized history session section.""" try: - sl = next(i for i, ln in enumerate(history) if ln.rstrip("\n") == "## Session log") + sl = next( + i + for i, ln in enumerate(history) + if ln.rstrip("\n") in HISTORY_SECTION_HEADINGS + ) except StopIteration as exc: - raise ValueError("history doc has no '## Session log' section") from exc + expected = "' or '".join(HISTORY_SECTION_HEADINGS) + raise ValueError(f"history doc has no '{expected}' section") from exc # skip the blank line after the header, insert before the first entry insert_at = sl + 1 while insert_at < len(history) and history[insert_at].strip() == "": diff --git a/scripts/tests/test_portability.py b/scripts/tests/test_portability.py index 15bf3e7..053d5e4 100644 --- a/scripts/tests/test_portability.py +++ b/scripts/tests/test_portability.py @@ -131,6 +131,73 @@ def test_archive_defaults_follow_configured_paths(tmp_path: Path) -> None: assert history == repo / "saved" / "handoff-history.md" +def test_archive_supports_recent_sessions_layout(tmp_path: Path) -> None: + archive = _load_module( + "archive_recent_sessions", ENGINE_DIR / "archive_plan_sessions.py" + ) + plan = tmp_path / "handoff.md" + history = tmp_path / "handoff-history.md" + plan.write_text( + """# Handoff + +## Last updated + +Current state. + +## Recent sessions + +### 2026-07-03 — Newest + +Newest body. + +--- + +### 2026-07-02 — Middle + +Middle body. + +--- + +### 2026-07-01 — Oldest + +Oldest body. + +--- + +## Strategic direction + +Standing content. +""", + encoding="utf-8", + ) + history.write_text( + """# Handoff history + +## Recent sessions (archived) + +### 2026-06-30 — Existing + +Existing body. +""", + encoding="utf-8", + ) + + result = archive.main( + ["--keep", "2", "--plan", str(plan), "--history", str(history)] + ) + + assert result == 0 + updated_plan = plan.read_text(encoding="utf-8") + updated_history = history.read_text(encoding="utf-8") + assert "### 2026-07-03 — Newest" in updated_plan + assert "### 2026-07-02 — Middle" in updated_plan + assert "### 2026-07-01 — Oldest" not in updated_plan + assert "## Strategic direction\n\nStanding content." in updated_plan + assert updated_history.index("### 2026-07-01 — Oldest") < updated_history.index( + "### 2026-06-30 — Existing" + ) + + def test_python_engine_root_walk_supports_namespacing(tmp_path: Path) -> None: repo = tmp_path / "project" nested_script = repo / "scripts" / "devkit" / "pr_watch.py"