From a312a30a58a5dc7df1577176447aaf393ba1c40c Mon Sep 17 00:00:00 2001 From: cody douglass Date: Thu, 6 Aug 2026 11:41:47 -0400 Subject: [PATCH 1/2] feat: add no-arg multi-repo mode to discover/plan, self-merging Finalize discover.sh and plan.sh now support running with no repo argument, fanning out across every repo under PROJECT_REPOS_DIR (or the subset that qualifies) in one interactive session instead of requiring one invocation per repo. - discover.sh (no arg): every repo under PROJECT_REPOS_DIR qualifies (no TASKS.md prerequisite), so it attaches the whole directory as one --add-dir; the skill enumerates repo subdirectories itself. - plan.sh (no arg): bash filters to repos with a TASKS.md containing unchecked items (same gate overnight.sh's run_repo uses) before claude ever launches, then attaches one --add-dir per qualifying repo. - Both skills gained a multi-repo mode: analysis fans out in parallel via Agent-tool dispatch (one per repo for discover, one per item across all attached repos for plan), then review/approval/write/ finalize happens serially, one repo at a time, in order. - Finalize in both skills now merges its own PR (gh pr merge --squash --delete-branch) once approved, then checks out the default branch and pulls latest. This closes a gap where plan couldn't see items discover just found (and overnight couldn't see plan's numbering) until someone merged the PR by hand. Scoped narrowly in .claude/settings.json to gh pr merge on chore/tasks-discover-* and chore/tasks-plan-* branches only -- raw git merge stays denied everywhere, and overnight.sh's task/housekeeping PRs are untouched. - README updated: new discover.sh section, expanded plan.sh section, no-arg forms in Scripts/Usage, and an explicit Safety model callout for the merge exception and its scope. --- .claude/settings.json | 4 +- .claude/skills/discover-tasks/SKILL.md | 13 ++++ .claude/skills/plan-tasks/SKILL.md | 29 +++++--- README.md | 93 +++++++++++--------------- discover.sh | 47 +++++++++---- plan.sh | 43 +++++++++--- 6 files changed, 140 insertions(+), 89 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index bdfe06a..e18c3dd 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -23,7 +23,9 @@ "Bash(git -C * log:*)", "Bash(git push origin overnight/*)", "Bash(git -C * push origin overnight/*)", - "Bash(gh pr create:*)" + "Bash(gh pr create:*)", + "Bash(gh pr merge chore/tasks-discover-*:*)", + "Bash(gh pr merge chore/tasks-plan-*:*)" ], "deny": [ "Bash(git push origin main:*)", diff --git a/.claude/skills/discover-tasks/SKILL.md b/.claude/skills/discover-tasks/SKILL.md index b19351e..07b0925 100644 --- a/.claude/skills/discover-tasks/SKILL.md +++ b/.claude/skills/discover-tasks/SKILL.md @@ -6,6 +6,16 @@ disable-model-invocation: true A target repo is attached via --add-dir. Two independent sourcing phases: memory-mining (always runs) and repo-scanning (only with `--scan`). +## Multi-repo mode + +Determine this from the attached directory's structure, never by assumption: does it have a `.git` at its own root? + +- Yes: it's a single target repo. Phases 1–4 below apply directly, exactly as written — this is today's behavior, unchanged. +- No, but its immediate subdirectories do: the attached directory is `PROJECT_REPOS_DIR` itself, and the repo set is those subdirectories. Run this flow instead of a single Phases 1–4 pass: + 1. Announce: "Found N repo(s) under ``." + 2. Dispatch one `general-purpose` agent per repo — all in a single message, foreground, batched in groups of ~8 if there are more than that — each told to run Phase 1 + Phase 2 steps 1–3 for its one assigned repo (absolute path stated explicitly) and report back its candidate list (task text, rationale, source) — report only, never ask, never write. Use `general-purpose`, not `Plan`: a dispatch needs Agent-tool access itself to reach `task-scout` when `--scan` is passed, and `Plan`-type agents don't have that. + 3. Once every dispatch returns, go through repos in the order they were listed. For each one: run Phase 2 step 4 (present its candidates, get my approval), then immediately Phase 3 (write) and Phase 4 (finalize) for that repo, before moving to the next. + Announce each phase to me in one line before starting it. ## Phase 1: locate memory @@ -42,5 +52,8 @@ Uncommitted content in the target repo's working tree is dangerous — lost work 6. Commit as `chore(tasks): discover N candidate task(s)`, body listing each item and its memory source. 7. Push, open a PR (`gh pr create`) targeting the default branch — title matches the commit, body summarizes the additions. 8. Report the PR URL. +9. Merge it: `gh pr merge --squash --delete-branch`, using the exact branch name from step 4 (not a PR number — that's what the scoped `chore/tasks-discover-*` merge permission matches against). This is safe specifically because every item going in was already explicitly approved by me in Phase 2 step 4 — the merge completes something already signed off on, it doesn't approve anything new. +10. Merge command fails (e.g. branch protection requires a review)? Report the failure plainly and leave the PR open. Don't retry, don't force, don't fall back to raw `git merge` — that stays denied regardless. +11. Merge succeeds: `git -C checkout` the default branch and pull latest, so the working tree's TASKS.md reflects the merge before this session ends — that's what lets a later `/plan-tasks` or `overnight.sh` run see these additions without anyone merging by hand. Do not implement any code in this session. Do not touch `docs/tasks-archive/` or `docs/nightlight-meta.json`. diff --git a/.claude/skills/plan-tasks/SKILL.md b/.claude/skills/plan-tasks/SKILL.md index 502938f..2e9238c 100644 --- a/.claude/skills/plan-tasks/SKILL.md +++ b/.claude/skills/plan-tasks/SKILL.md @@ -6,13 +6,17 @@ disable-model-invocation: true A target repo is attached via --add-dir. Read its TASKS.md, CLAUDE.md, and docs/nightlight-meta.json (if present — `nextTaskNumber` is the next number to assign; treat a missing file as `nextTaskNumber: 1`). -`## Discovered` holds raw, un-triaged candidates (no `#`, no `[stack]`) — they're in scope below alongside native Agent-Ready/Verify/Research/Decisions items. +## Multi-repo mode + +More than one repo attached via --add-dir (`plan.sh`'s no-arg mode passes one `--add-dir` per repo that has open, unchecked `TASKS.md` work): read TASKS.md/CLAUDE.md/docs/nightlight-meta.json for every attached repo, keeping each repo's state separate — `#` numbering, `[stack]` names, and `nextTaskNumber` are all per-repo, never shared or cross-referenced across repos. Investigate (parallel) below dispatches across every attached repo's items in one combined batch; Synthesize then runs once per repo, in the order the repos were attached, each with its own approval, write, and finalize before moving to the next. One repo attached: today's flow, unchanged. + +`## Discovered` holds raw, un-triaged candidates (no `#`, no `[stack]`) — they're in scope below alongside native Agent-Ready/Verify/Research/Decisions items, for every attached repo. ## Investigate (parallel) -Every unchecked item (including Discovered ones) gets its own investigation — dispatch one `Plan`-type agent per item, all as parallel foreground tool calls in a single message. Never investigate items one at a time in the main thread; that's the slow path this phase replaces. If there are more than ~8 items, dispatch in batches of that size rather than serially one-by-one. +Every unchecked item across every attached repo (including Discovered ones) gets its own investigation — dispatch one `Plan`-type agent per item, all as parallel foreground tool calls in a single message, regardless of which repo it belongs to. Never investigate items one at a time in the main thread; that's the slow path this phase replaces. If there are more than ~8 items total, dispatch in batches of that size rather than serially one-by-one. -Each dispatch is self-contained (the agent has no memory of this session) and must state: the item's exact text, the target repo's absolute path, and that it should read that repo's CLAUDE.md itself for conventions. Ask each agent to report back — never implement — with: +Each dispatch is self-contained (the agent has no memory of this session) and must state: the item's exact text, its repo's absolute path, and that it should read that repo's CLAUDE.md itself for conventions. Ask each agent to report back — never implement — with: 1. For a Discovered item: which section it actually belongs in — Agent-Ready (the common case), Verify, Research, or Decisions (flag for my judgment). 2. A sub-task breakdown with file-level hints and any missing acceptance criteria. @@ -21,21 +25,23 @@ Each dispatch is self-contained (the agent has no memory of this session) and mu ## Synthesize -Once every dispatched investigation has returned: +Once every dispatched investigation has returned, process repos one at a time, in the order they were attached (single-repo mode: just the one). For each repo: -1. Cross-reference reported touch-points and dependencies across all items: share `[stack: ]`, in execution order, if they depend on each other or touch the same files; independent items get `[stack: solo]`. When in doubt, stack. -2. Assign each item a stable `#`, starting from `nextTaskNumber` and incrementing per item, in write order. +1. Cross-reference reported touch-points and dependencies across that repo's items only — stacks and numbering never cross repos: share `[stack: ]`, in execution order, if they depend on each other or touch the same files; independent items get `[stack: solo]`. When in doubt, stack. +2. Assign each item a stable `#`, starting from that repo's own `nextTaskNumber` and incrementing per item, in write order. 3. Propose where each item lands in its target section's existing list — default to the bottom, but ask me explicitly where I want each one; I may want something worked first (e.g. a quick bug fix ahead of a bigger feature). Present the full proposal for my approval BEFORE writing anything — triage decisions (for former Discovered items), breakdowns/numbers/stack tags, and proposed position per item. After I approve: -- Write the breakdowns, `#` numbers, and `[stack]` tags into TASKS.md at the approved position — every checkbox line gets its number right after the checkbox, before the stack tag (e.g. `- [ ] #48 [stack: auth] Add rate limiting to login endpoint`). -- Remove triaged items from `## Discovered`. -- Update `docs/nightlight-meta.json`'s `nextTaskNumber` to one past the highest number assigned (create it with `nextTaskNumber`, `tasksCompleted: 0`, `tasksBlocked: 0` if missing). +- Write the breakdowns, `#` numbers, and `[stack]` tags into that repo's TASKS.md at the approved position — every checkbox line gets its number right after the checkbox, before the stack tag (e.g. `- [ ] #48 [stack: auth] Add rate limiting to login endpoint`). +- Remove triaged items from that repo's `## Discovered`. +- Update that repo's `docs/nightlight-meta.json`'s `nextTaskNumber` to one past the highest number assigned (create it with `nextTaskNumber`, `tasksCompleted: 0`, `tasksBlocked: 0` if missing). + +Then immediately run Finalize (below) for that repo before starting the next repo's Synthesize. ## Finalize -Uncommitted content in the target repo's working tree is dangerous — lost work, or silently swept into unrelated commits. Don't leave the TASKS.md/nightlight-meta.json edits uncommitted. Once written: +Uncommitted content in the target repo's working tree is dangerous — lost work, or silently swept into unrelated commits. Don't leave the TASKS.md/nightlight-meta.json edits uncommitted. Once written, for this repo: 1. Confirm planning is done and I want these changes committed + PR'd. Don't proceed without an explicit yes. 2. Resolve the default branch: `git -C symbolic-ref --quiet --short refs/remotes/origin/HEAD` (strip `origin/`), falling back to local `main` then `master` — same resolution `overnight.sh`'s `default_branch()` uses. @@ -45,5 +51,8 @@ Uncommitted content in the target repo's working tree is dangerous — lost work 6. Commit as `chore(tasks): number and stack N task(s)`, body listing the task numbers and stack names assigned. 7. Push, open a PR (`gh pr create`) targeting the default branch — title matches the commit, body summarizes the assignments. 8. Report the PR URL. +9. Merge it: `gh pr merge --squash --delete-branch`, using the exact branch name from step 4 (not a PR number — that's what the scoped `chore/tasks-plan-*` merge permission matches against). This is safe specifically because the breakdown/numbering/stacking going in was already explicitly approved by me in Synthesize — the merge completes something already signed off on, it doesn't approve anything new. +10. Merge command fails (e.g. branch protection requires a review)? Report the failure plainly and leave the PR open. Don't retry, don't force, don't fall back to raw `git merge` — that stays denied regardless. +11. Merge succeeds: `git -C checkout` the default branch and pull latest, so the working tree's TASKS.md reflects the merge before this repo's finalize is done — that's what lets a later `overnight.sh` run see the numbering/stacking without anyone merging by hand. Do not implement any code in this session. diff --git a/README.md b/README.md index f1fc092..8505025 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,11 @@ Repo location is set once, in a gitignored `.env` at the root: PROJECT_REPOS_DIR=/Users/you/Documents/project-repos ``` -Both `overnight.sh` and `plan.sh` source this, so the path only lives in one place. Copy `.env.example` to `.env` and edit it after cloning; `.env` itself is gitignored alongside `logs/`. +`overnight.sh`, `plan.sh`, and `discover.sh` all source this, so the path only lives in one place. Copy `.env.example` to `.env` and edit it after cloning; `.env` itself is gitignored alongside `logs/`. This `.env` only ever holds `PROJECT_REPOS_DIR` — a path, not a secret — and gets sourced by the shell scripts before `claude` ever launches, so it's unrelated to the `Read(.env)` / `Read(.env.*)` deny rule in `.claude/settings.json`. That rule stops the *agent* from reading `.env` files inside whatever target repo it's working in, which may hold real credentials. -**Why not Claude Code's own `additionalDirectories` setting instead?** It exists (`.claude/settings.json` or the gitignored `.claude/settings.local.json`) and is the persisted equivalent of `--add-dir` — no CLI flag needed, ever. But it only accepts static paths, and pointing it at the whole `project-repos` folder grants every session access to every repo under it, not just the one you're working on. That's a real convenience if you don't mind the blanket access. This repo defaults to the narrower option instead: a wrapper script that resolves one repo name to one path per session, staying consistent with how `overnight.sh` already scopes each run to a single target. +**Why not Claude Code's own `additionalDirectories` setting instead?** It exists (`.claude/settings.json` or the gitignored `.claude/settings.local.json`) and is the persisted equivalent of `--add-dir` — no CLI flag needed, ever. But it's a *standing* grant: every session launched from this repo would have it, forever, whether or not that session has anything to do with any target repo. This repo defaults to the narrower option instead: a wrapper script that resolves repo path(s) fresh per invocation, scoped to what that one run is actually doing. `discover.sh`'s no-arg mode (below) does attach the whole `PROJECT_REPOS_DIR` — but only for that one process's lifetime, and only because discover has no filter to narrow it with (see `## discover.sh`); `plan.sh`'s no-arg mode attaches only the repos that actually have work to plan. ## Setup @@ -41,29 +41,34 @@ This `.env` only ever holds `PROJECT_REPOS_DIR` — a path, not a secret — and 3. Edit `CLAUDE.md` here if you want to change the workflow rules (branching scheme, quality gates, how research tasks work, etc.) — the defaults are a reasonable starting point. 4. Edit `.claude/settings.json` to match your actual test/lint/build commands (defaults assume npm/pnpm). 5. In each target repo, add a `TASKS.md` with your work queue and confirm `CLAUDE.md` has a test command and conventions documented. That's the only setup required in the target repo itself. -6. `chmod +x overnight.sh plan.sh stats.sh`. No `pnpm install` needed — `package.json` has no dependencies, it's just script aliases. +6. `chmod +x overnight.sh plan.sh discover.sh stats.sh`. No `pnpm install` needed — `package.json` has no dependencies, it's just script aliases. -## plan.sh +## discover.sh -Wrapper for interactive planning sessions, so `/plan-tasks` runs against the right repo with zero manual steps: +Wrapper for `/discover-tasks`, so it runs against the right repo (or repos) with zero manual steps: ```bash -#!/bin/bash -set -e -source "$(dirname "$0")/.env" -REPO="${1:?usage: plan.sh }" -TARGET="$PROJECT_REPOS_DIR/$REPO" -[[ -d "$TARGET" ]] || { echo "not found: $TARGET"; exit 1; } -exec claude --add-dir "$TARGET" "/plan-tasks" +./discover.sh some-repo # single repo +./discover.sh some-repo --scan # single repo, plus a codebase scan +./discover.sh # every repo under PROJECT_REPOS_DIR ``` +Same "adds the target repo and submits the slash command as the first message" shape as `plan.sh` below — a normal interactive session, full back-and-forth, nothing written until you approve. The no-arg form attaches the whole `PROJECT_REPOS_DIR` as one `--add-dir` (discover has no `TASKS.md` prerequisite, so every repo qualifies — see the `additionalDirectories` note above for why that's fine here specifically), then mines every repo's memory in parallel and walks you through approving each repo's candidates one at a time, in order. Each repo's Finalize step now also merges its own PR once you've approved it — see Safety model below. + +Or via the `package.json` script (see Scripts below): `pnpm discover [repo] [--scan]`. + +## plan.sh + +Wrapper for interactive planning sessions, so `/plan-tasks` runs against the right repo (or repos) with zero manual steps: + ```bash -./plan.sh some-repo +./plan.sh some-repo # single repo +./plan.sh # every repo under PROJECT_REPOS_DIR with open, unchecked TASKS.md work ``` -This adds the target repo *and* submits `/plan-tasks` as the session's first message in one command — `claude ""` (no `-p`) starts a normal interactive session with that message pre-submitted, then stays interactive, so you still get the full back-and-forth: the proposal comes back, you review it, and only after you approve does it write to `TASKS.md`. Nothing about the approval step changes, you just skip typing the path and the command by hand. +`claude ""` (no `-p`) starts a normal interactive session with `/plan-tasks` pre-submitted as the first message, then stays interactive: the proposal comes back, you review it, and only after you approve does it write to `TASKS.md`. The no-arg form first checks every repo under `PROJECT_REPOS_DIR` for a `TASKS.md` with unchecked items (same check `overnight.sh`'s no-arg mode uses), attaches only the ones that qualify, investigates every repo's items in parallel, then walks you through approving and finalizing each qualifying repo in turn. Each repo's Finalize step now also merges its own PR once you've approved it — see Safety model below. -Or via the `package.json` script (see Scripts below): `pnpm plan some-repo`. +Or via the `package.json` script (see Scripts below): `pnpm plan [repo]`. ## stats.sh @@ -81,49 +86,24 @@ Local, gitignored cost/time reference — never committed, never touches a targe `pnpm stats some-repo` prints that repo's running totals. `pnpm stats` (no arg) sums the `total_*` fields across every `stats/*.json` file, for a comprehensive total across every repo worked from this nightlight instance. It's a snapshot, not a log — each run overwrites the file with updated totals, so there's nothing to grep through, just current numbers. -## .claude/skills/plan-tasks/SKILL.md - -The `/plan-tasks` skill does the task breakdown that makes the overnight run actually work — `overnight.sh` executes a queue, it doesn't design one. Run this before every session, any time `TASKS.md` has new or vague items in it. - -```markdown ---- -name: plan-tasks -description: Plans and stack-annotates a target repo's TASKS.md. Use when starting a planning session to break down Agent-Ready tasks, assign [stack] annotations, and flag prerequisites before an overnight run. -disable-model-invocation: true ---- - -A target repo is attached to this session via --add-dir. Read its TASKS.md, its CLAUDE.md, and its docs/nightlight-meta.json (if present — its `nextTaskNumber` field is the next number to assign; treat a missing file as `nextTaskNumber: 1`). For each unchecked item across every section (Agent-Ready, Verify, Research, Decisions): - -1. Investigate the relevant parts of the target repo's codebase. -2. Propose a sub-task breakdown with file-level hints and any missing acceptance criteria. -3. Flag missing prerequisites (fixtures, env vars, dependencies) as NEEDS HUMAN annotations. -4. Analyze dependencies AND shared-file overlap between tasks (schemas, barrel exports, shared components), then propose a [stack: ] annotation for every task: tasks that depend on each other or touch the same files share a stack name in execution order; genuinely independent tasks get [stack: solo]. When in doubt, stack. -5. Assign each task in the proposal a stable `#` number, starting from `nextTaskNumber` and incrementing by one per task, in the order they'll be written into TASKS.md. - -Present the full proposal for my approval BEFORE writing anything. After I approve: - -- Write the breakdowns, `#` numbers, and `[stack]` annotations into the target repo's TASKS.md — every checkbox line gets its number right after the checkbox, before the stack tag (e.g. `- [ ] #48 [stack: auth] Add rate limiting to login endpoint`). -- Update `docs/nightlight-meta.json`'s `nextTaskNumber` to one past the highest number just assigned (create the file with `nextTaskNumber`, `tasksCompleted: 0`, and `tasksBlocked: 0` if it doesn't exist yet). - -Do not implement any code in this session. -``` - -Skills live in a named folder with a `SKILL.md` inside (not a flat `.md` file directly under `.claude/skills/`) — that's the format Claude Code actually discovers. `disable-model-invocation: true` means it only runs when explicitly called via `/plan-tasks`, never auto-triggered. +## .claude/skills/plan-tasks/SKILL.md and .claude/skills/discover-tasks/SKILL.md -It matters for three reasons: +`/plan-tasks` does the task breakdown that makes the overnight run actually work — `overnight.sh` executes a queue, it doesn't design one. `/discover-tasks` mines a repo's own memory (and, with `--scan`, its codebase) for task candidates before that. The full, current behavior of each lives in its `SKILL.md` (not reproduced here — this file drifted out of sync with the skills once before and isn't worth re-duplicating), but the shape of both: -- **Acceptance criteria and file-level hints.** A raw task like "add rate limiting" is too vague to run unattended safely — this turns it into something with a defined "done." -- **`[stack: ]` assignment.** This is what makes the overnight PRs mergeable in the right order. It's driven by real dependency and shared-file analysis (schemas, barrel exports, shared components), not guessed at — and it's deliberately biased toward stacking ("when in doubt, stack") over declaring things independent, since a false `solo` risks two PRs conflicting on the same file with no ordering to resolve it. -- **`#` numbering.** Gives every task a stable identity that survives being checked off and archived, so a PR, commit, or `## Discovered` note can reference "task #48" and still mean the same thing months later. +- Investigate/mine in parallel (one dispatched agent per item or per repo), then synthesize and ask for approval back in the main thread — never one-at-a-time in the main thread, and never write anything before you've approved it. +- `#` numbering and `[stack: ]` assignment (`plan-tasks`) — a stable identity per task, and the dependency/shared-file analysis that makes stacked overnight PRs mergeable in the right order. Deliberately biased toward stacking ("when in doubt, stack") since a false `solo` risks two PRs conflicting with no ordering to resolve it. +- No-arg mode (both): fans out across every repo under `PROJECT_REPOS_DIR` (or the ones that qualify — see `## discover.sh` / `## plan.sh` above) in parallel, then reviews and finalizes them one at a time, in order. +- Finalize: commits, opens a PR, and — new — merges it once you've approved the content earlier in that same session. See Safety model below for the scope of that. -Runs interactively, proposes before writing anything, and never implements — it only edits `TASKS.md`. `plan.sh` (below) launches it directly. +Skills live in a named folder with a `SKILL.md` inside (not a flat `.md` file directly under `.claude/skills/`) — that's the format Claude Code actually discovers. `disable-model-invocation: true` means each only runs when explicitly called via its slash command, never auto-triggered. Both run interactively and never touch application code — only `TASKS.md` and `docs/nightlight-meta.json`. ## Scripts ```bash -pnpm plan some-repo # ./plan.sh some-repo -pnpm overnight some-repo # ./overnight.sh some-repo -pnpm stats [some-repo] # ./stats.sh [some-repo] +pnpm discover [repo] [--scan] # ./discover.sh [repo] [--scan] +pnpm plan [repo] # ./plan.sh [repo] +pnpm overnight some-repo # ./overnight.sh some-repo +pnpm stats [some-repo] # ./stats.sh [some-repo] ``` Thin `package.json` wrappers around the shell scripts — no dependencies, nothing to `pnpm install`. Args pass straight through to the script (pnpm doesn't need a `--` separator the way npm does), so `pnpm plan some-repo` and `./plan.sh some-repo` are identical. Use whichever reads better; the rest of this README uses the raw `./script.sh` form since it's unambiguous about what's actually running. @@ -131,13 +111,18 @@ Thin `package.json` wrappers around the shell scripts — no dependencies, nothi ## Usage ```bash -# breakdown first (see plan.sh above) if TASKS.md has anything new or vague +# mine memory for task candidates first, if any have accumulated +./discover.sh some-repo + +# breakdown next (see plan.sh above) if TASKS.md has anything new or vague ./plan.sh some-repo # the actual overnight run ./overnight.sh some-repo ``` +`discover.sh`/`plan.sh` with no repo run across every repo under `PROJECT_REPOS_DIR` (see `## discover.sh` / `## plan.sh` above) — but they're still two separate commands, run whenever you want; discover doesn't automatically feed into plan. Since discover's Finalize now merges its own PR (see Safety model below), running `plan` any time after `discover` already sees whatever discover found, with no manual merge step in between. + `overnight.sh` resolves a bare name against `$PROJECT_REPOS_DIR` (from `.env`) or accepts a full path. Run it inside `tmux` or a terminal window you can leave open — closing the window kills the process. Disable sleep/hibernate for the duration. Only run one repo at a time per machine (shared usage pool). Chain sequentially if you need more than one: `./overnight.sh repo-a && ./overnight.sh repo-b`. @@ -217,6 +202,8 @@ Merge stacks bottom-up. Merge solos and housekeeping whenever. No `--dangerously-skip-permissions`. `.claude/settings.json` is an explicit allowlist (test/lint/build commands, safe git operations, PR creation) plus an explicit denylist (`main`/`master` push, merge, `rm -rf`, `.env` reads, `--no-verify`). The agent can't merge its own work or push to a protected branch — everything it produces is a PR waiting for you to look at it. If a task needs judgment or credentials it doesn't have, it's supposed to stop and annotate rather than guess. +**One narrow, deliberate exception:** `/discover-tasks` and `/plan-tasks` Finalize each merge their own PR (`gh pr merge --squash --delete-branch`) once it's open, rather than leaving it for you to merge by hand. This isn't the agent deciding what's safe to land — every item going into that PR was already explicitly approved by you earlier in that same session (candidate-by-candidate in discover's Phase 2, or the full breakdown/numbering/stacking proposal in plan's Synthesize). The merge just completes something you already signed off on. It's scoped tightly on purpose: `.claude/settings.json` only allows `gh pr merge` for branches matching `chore/tasks-discover-*` / `chore/tasks-plan-*` — raw `git merge` stays denied everywhere, for every branch, no exceptions — and those branches only ever touch `TASKS.md`/`docs/nightlight-meta.json`, never application code. `overnight.sh`'s task PRs and its end-of-session housekeeping PR are untouched by this — those still always wait for you, exactly as before. + ## Prior art This isn't the only take on "run Claude Code overnight." A few others, for comparison: @@ -226,4 +213,4 @@ This isn't the only take on "run Claude Code overnight." A few others, for compa - [continuous-claude](https://github.com/AnandChowdhary/continuous-claude) — a literal loop: PR, wait for checks, merge, repeat. - [Boucle](https://github.com/Bande-a-Bonnot/Boucle-framework) — cron-scheduled, with its own persistent-memory layer across runs. -The main difference here: this system never bypasses permissions and never merges its own PRs. Everything above defaults to skipping permission prompts for the autonomous run; this one trades some autonomy for an explicit allowlist and human-reviewed merges instead. +The main difference here: this system never bypasses permissions, and code changes always wait for a human-reviewed merge. Everything above defaults to skipping permission prompts for the autonomous run; this one trades some autonomy for an explicit allowlist instead — the only self-merges it ever does are `discover`/`plan`'s own already-approved `TASKS.md` bookkeeping (see Safety model above), never application code. diff --git a/discover.sh b/discover.sh index 66f9395..416adcc 100644 --- a/discover.sh +++ b/discover.sh @@ -1,6 +1,10 @@ #!/bin/bash -# usage: pnpm discover [--scan] — mines the target repo's own memory (and, -# with --scan, its codebase) for task candidates and proposes additions to its TASKS.md +# usage: pnpm discover [repo] [--scan] +# with repo: mine that one repo's memory (and, with --scan, its codebase) +# for task candidates and propose additions to its TASKS.md +# without: same, but for every repo under PROJECT_REPOS_DIR -- discover +# has no TASKS.md prerequisite (it can create one), so every +# repo qualifies, no filtering needed set -e cd "$(dirname "$0")" @@ -9,17 +13,32 @@ git pull --quiet || echo "warn: could not pull latest nightlight rules, running set -a; source .env; set +a : "${PROJECT_REPOS_DIR:?PROJECT_REPOS_DIR not set in .env}" -REPO="${1:?usage: discover.sh [--scan]}" -[[ -d "$REPO" ]] || REPO="$PROJECT_REPOS_DIR/$REPO" -# resolve to an absolute path so "." and "./" (e.g. self-targeting nightlight -# from inside its own directory) end up byte-identical before reaching --add-dir. -# pwd -W (not plain pwd) keeps the Windows-style uppercase drive letter -- -# plain pwd's POSIX-style lowercase-drive path (/c/Users/...) doesn't match -# the casing Claude Code's directory-trust store uses elsewhere, so trust -# never "sticks" and every subrepo re-prompts on every run. -REPO="$(cd "$REPO" && pwd -W)" - +REPO="" SCAN_FLAG="" -[[ "$2" == "--scan" ]] && SCAN_FLAG=" --scan" +for arg in "$@"; do + if [[ "$arg" == "--scan" ]]; then + SCAN_FLAG=" --scan" + else + REPO="$arg" + fi +done -MSYS_NO_PATHCONV=1 claude "/discover-tasks$SCAN_FLAG" --add-dir "$REPO" +if [[ -n "$REPO" ]]; then + [[ -d "$REPO" ]] || REPO="$PROJECT_REPOS_DIR/$REPO" + # resolve to an absolute path so "." and "./" (e.g. self-targeting nightlight + # from inside its own directory) end up byte-identical before reaching --add-dir. + # pwd -W (not plain pwd) keeps the Windows-style uppercase drive letter -- + # plain pwd's POSIX-style lowercase-drive path (/c/Users/...) doesn't match + # the casing Claude Code's directory-trust store uses elsewhere, so trust + # never "sticks" and every subrepo re-prompts on every run. + REPO="$(cd "$REPO" && pwd -W)" + MSYS_NO_PATHCONV=1 claude "/discover-tasks$SCAN_FLAG" --add-dir "$REPO" +else + # No repo given and no filter exists for discover, so every repo under + # PROJECT_REPOS_DIR is in scope -- attach the whole folder as one --add-dir + # rather than building one flag per repo (identical access either way when + # nothing gets excluded). The /discover-tasks skill enumerates the repo + # subdirectories itself once it's running. + PROJECT_REPOS_DIR="$(cd "$PROJECT_REPOS_DIR" && pwd -W)" + MSYS_NO_PATHCONV=1 claude "/discover-tasks$SCAN_FLAG" --add-dir "$PROJECT_REPOS_DIR" +fi diff --git a/plan.sh b/plan.sh index c8e1534..029da7e 100644 --- a/plan.sh +++ b/plan.sh @@ -1,5 +1,8 @@ #!/bin/bash -# usage: pnpm plan — opens an interactive planning session on the target repo +# usage: pnpm plan [repo] — opens an interactive planning session +# with repo: plans that one repo +# without: plans every repo under PROJECT_REPOS_DIR that has open, +# unchecked TASKS.md work (same gate overnight.sh's run_repo uses) set -e cd "$(dirname "$0")" @@ -8,14 +11,32 @@ git pull --quiet || echo "warn: could not pull latest nightlight rules, running set -a; source .env; set +a : "${PROJECT_REPOS_DIR:?PROJECT_REPOS_DIR not set in .env}" -REPO="${1:?usage: plan.sh }" -[[ -d "$REPO" ]] || REPO="$PROJECT_REPOS_DIR/$REPO" -# resolve to an absolute path so "." and "./" (e.g. self-targeting nightlight -# from inside its own directory) end up byte-identical before reaching --add-dir. -# pwd -W (not plain pwd) keeps the Windows-style uppercase drive letter -- -# plain pwd's POSIX-style lowercase-drive path (/c/Users/...) doesn't match -# the casing Claude Code's directory-trust store uses elsewhere, so trust -# never "sticks" and every subrepo re-prompts on every run. -REPO="$(cd "$REPO" && pwd -W)" +REPO="$1" -MSYS_NO_PATHCONV=1 claude "/plan-tasks" --add-dir "$REPO" \ No newline at end of file +if [[ -n "$REPO" ]]; then + [[ -d "$REPO" ]] || REPO="$PROJECT_REPOS_DIR/$REPO" + # resolve to an absolute path so "." and "./" (e.g. self-targeting nightlight + # from inside its own directory) end up byte-identical before reaching --add-dir. + # pwd -W (not plain pwd) keeps the Windows-style uppercase drive letter -- + # plain pwd's POSIX-style lowercase-drive path (/c/Users/...) doesn't match + # the casing Claude Code's directory-trust store uses elsewhere, so trust + # never "sticks" and every subrepo re-prompts on every run. + REPO="$(cd "$REPO" && pwd -W)" + MSYS_NO_PATHCONV=1 claude "/plan-tasks" --add-dir "$REPO" +else + # No repo given: only attach repos that actually have unplanned work -- a + # TASKS.md with at least one unchecked item -- same gate overnight.sh's + # run_repo uses. Computed here in plain bash, before claude ever launches, + # so repos without qualifying work are never attached at all. + ADD_DIRS=() + for d in "$PROJECT_REPOS_DIR"/*/; do + d="${d%/}" + name="$(basename "$d")" + tasks="$d/TASKS.md" + [[ -f "$tasks" ]] || { echo "skip: $name (no TASKS.md)"; continue; } + grep -Eq '^\s*- \[ \]' "$tasks" || { echo "skip: $name (no open tasks)"; continue; } + ADD_DIRS+=(--add-dir "$(cd "$d" && pwd -W)") + done + [[ ${#ADD_DIRS[@]} -gt 0 ]] || { echo "no repos under PROJECT_REPOS_DIR have open tasks"; exit 0; } + MSYS_NO_PATHCONV=1 claude "/plan-tasks" "${ADD_DIRS[@]}" +fi From 20a82c41e9d404a62a8259cea6b09a7c13eb8720 Mon Sep 17 00:00:00 2001 From: cody douglass Date: Thu, 6 Aug 2026 12:10:02 -0400 Subject: [PATCH 2/2] fix(settings): allow bookkeeping branch push and pull for discover/plan Finalize in both discover-tasks and plan-tasks pushes chore/tasks-discover-* /chore/tasks-plan-* branches and runs git -C pull post-merge, but neither was in the allowlist -- only overnight/* pushes were. Adds narrow allow rules for both branch patterns (plain and -C * forms, matching the existing overnight/* pattern) and for git pull, which doesn't need branch scoping since it can't push anything. Flagged by CodeRabbit on #20. --- .claude/settings.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.claude/settings.json b/.claude/settings.json index e18c3dd..033f2aa 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -23,6 +23,12 @@ "Bash(git -C * log:*)", "Bash(git push origin overnight/*)", "Bash(git -C * push origin overnight/*)", + "Bash(git push origin chore/tasks-discover-*)", + "Bash(git -C * push origin chore/tasks-discover-*)", + "Bash(git push origin chore/tasks-plan-*)", + "Bash(git -C * push origin chore/tasks-plan-*)", + "Bash(git pull:*)", + "Bash(git -C * pull:*)", "Bash(gh pr create:*)", "Bash(gh pr merge chore/tasks-discover-*:*)", "Bash(gh pr merge chore/tasks-plan-*:*)"