From 4e27dd8920e998977d2fb7535b5631c2f1b4a1b4 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 28 Jul 2026 22:17:33 +0000 Subject: [PATCH] feat(plan): discontinue `plan run`; a plan is delivered in your session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The autonomous loop cost an order of magnitude more than the same work done interactively and frequently delivered nothing. That is its design, not a defect in it: a session was rebuilt from zero per feat AND per attempt, so every discovery was re-paid at orchestrator prices with a cold cache, and a `done` the verdict gate refused threw away a session already paid for in full. So the command answers with a refusal that names where the work moved — `plan next` picks the feat, `plan brief` is its context, the `plan-dev` skill is the method. Everything else about plans is unchanged: init, list, validate, approve, status, next, brief, generate. `internal/plan` — runner, worktrees, ledger, watchdog, verdict gate — is untouched and still tested. The idea is on standby, and reviving it should be a revert rather than a rewrite. Three consequences follow, and they are the actual change: The human gate is unconditional again. The "one sanctioned exception" in the root CLAUDE.md let a plan session approve its own validated spec phases; it existed only because the loop had no human in it. Every phase is approved by a human now, and `plan-dev` presents artifacts instead of approving them. The brief is context and nothing else. The role preamble and the verdict protocol addressed a headless session whose entire prompt was that text. A session reading a brief now has its own CLAUDE.md, its skills and a human in the room, so the brief holds what only the plan knows — the feat, its governors, where it lives, what is already there — and stops. Every shipped artifact stops naming the command. It was in a skill, three sandbox files, two PRD surfaces, an agent and the root CLAUDE.md, and an instruction that survives in a shipped artifact is one a model will follow. TestNoShippedArtifactMentionsPlanRun sweeps them so it cannot creep back. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ARBSaXgJ7ssW1hvpy6r64y --- README.md | 64 +++--- internal/cli/cli.go | 6 +- internal/cli/plan.go | 191 +++--------------- internal/cli/plan_bridge_cli_test.go | 32 +-- internal/cli/plan_cli_test.go | 30 +-- internal/cli/plan_skill_test.go | 1 - internal/cli/plan_telegram_test.go | 87 -------- internal/cli/sandbox.go | 2 +- internal/cli/spec.go | 4 +- internal/cli/telegram.go | 2 +- internal/plan/brief.go | 48 ++--- internal/plan/brief_test.go | 44 +--- internal/templater/templater_test.go | 49 +++++ .../templates/agents/quality-gate.md.tmpl | 10 +- .../templates/commands/csdd-plan-run.md.tmpl | 59 ------ .../templater/templates/commands/prd.md.tmpl | 4 +- .../templater/templates/plan/plan.md.tmpl | 9 +- .../templater/templates/root/CLAUDE.md.tmpl | 10 +- .../templates/root/knowledge-section.md.tmpl | 11 +- .../templates/sandbox/Dockerfile.tmpl | 4 +- .../templates/sandbox/devcontainer.json.tmpl | 6 +- .../templates/sandbox/init-firewall.sh.tmpl | 2 +- .../templates/skills/plan-dev/SKILL.md.tmpl | 167 +++++++-------- .../templates/skills/prd/SKILL.md.tmpl | 10 +- 24 files changed, 277 insertions(+), 575 deletions(-) delete mode 100644 internal/cli/plan_telegram_test.go delete mode 100644 internal/templater/templates/commands/csdd-plan-run.md.tmpl diff --git a/README.md b/README.md index 3f3be1d..d1bffa1 100644 --- a/README.md +++ b/README.md @@ -291,56 +291,42 @@ A `plan.md` carries three machine-parsed sections that `csdd plan validate` chec The `Refs` column is how a feat cites its *why*: `[[wiki-page]]`, `stack:`, and `adr:` tokens — each must resolve, or validation breaks. Cited decision records are inlined **in full** into that feat's run brief. -### Two human gates, machine work between +### Delivering a plan in your session ``` -csdd plan approve ──▶ csdd plan run ──▶ Pull Request - (human gate) (csdd-controlled) (human gate) +csdd plan approve ──▶ you deliver, feat by feat ──▶ Pull Request + (human gate) (your Claude Code session) (human gate) ``` -`csdd plan run ` is a **deliberately dumb, Ralph-style loop** where **one iteration = one Claude session** (default cap 100), and each session is handed **one whole feat as its mission**. The session — Claude driving csdd — owns the entire flow: it authors and approves the spec (`csdd spec generate/validate/approve`), implements every task, runs the dev-cycle, owns git (branch, `/csdd-commit`, the pre-push gate, the PR), and records any open decision it makes (a `docs/stack.md` Decided row, an ADR when the why needs more than a line). **The loop trusts the session and does not verify** — there is no runner-side gate. It hands out feats in plan order, spawns the session, and records what it declares in a per-feat ledger (`.csdd/plan//progress.json`). Nothing parks mid-run — **every failure becomes the next session's context** (the failure trail, the predecessor's handoff), which is what makes the loop self-correcting. - -The session's verdict declares one of three intents: `done` (the whole feat is delivered and the session's own checks — `csdd spec validate`, `csdd graph analyze --strict`, the plan's Quality Gates — pass), `continue` (honest partial work; the summary is the handoff to the next session on the same feat), or `blocked` (the feat turned out to need another feat of this plan that has not landed — it parks without spending an attempt, and the discovered edge is recorded in `.csdd/plan//discovered-deps.json`, never written back into `plan.md`). - -What a session is handed is **the feat, not the method**. The brief opens by naming the role it is written for — the whole thing is fed to `claude -p` as the prompt, so its first lines are where the session's role is set — then carries the feat row, its governing refs, the seeds, the Executor Notes and the plan's Quality Gates; the development process — the session's authority to approve its own phases, the cycle, what it delegates, the git it owns and what makes a `done` acceptable — lives in the plan-session `CLAUDE.md` written into each worktree and in the `plan-dev` skill, both of which the session reads every turn anyway. - -Before a feat is dispatched, a **context pass** (`--enrich-model`, default `sonnet`) reads its worktree once and records what the feat touches, which decisions and stack rows govern it, what is already there and what is not, into `.csdd/plan//briefs/.json`. Every path and every `adr:`/`stack:` citation is checked against the workspace before it is stored — an unverifiable claim is dropped, not rendered — and the pack is keyed to the feat row, so a feat's later attempts reuse it instead of rediscovering the same tree at the orchestrator's price. `csdd plan brief --feat F` prints the brief and runs the pass itself when the feat has no pack yet, which is how you review (and, by editing the JSON, correct) what a session will be handed. A pack already on disk is reused as-is — even when the feat's row has since changed, which is only reported, since briefing is something you do repeatedly while editing a plan and regenerating costs a model call; `--refresh` is how you replace it. `--enrich-model none` turns the pass off and briefs from the plan alone. - -Feats are handed out in **dependency order**, not table order: the `Depends` column the plan already declares is what schedules the run, so a feat is only dispatched once everything it builds on is delivered. If a feat is blocked, everything downstream of it is reported as unreachable with the root cause named, instead of being dispatched into a tree where its foundation does not exist. - -**A serial run works in your checkout.** At `--squad-limit 1` (the default) one session runs at a time, in the repository itself — the environment your suite needs is already installed there. The worktrees below are what a *squad* pays for isolation: a worktree carries only tracked files, so it starts without `node_modules/`, `.venv/` or any other build cache git ignores, and rebuilding those per feat buys a serial run nothing, since it has no peer to collide with. - -`--squad-limit N` (2..6) runs up to **N sessions at once, each on its own feat and each in its own git worktree**. Filesystem isolation is what makes it safe: every concurrent session gets a worktree under `.csdd/plan//trees/` on branch `csdd//`, so no two agents ever share an index, a build, or a half-written file. Scheduling is the `Depends` graph alone — the `(P)` column is **not** consulted, because it used to mean "consents to share a working tree" and feats no longer share one. - -Because a plan is a DAG, isolation alone is not enough: `c` depends on `a` and `b`, and a worktree cut from an untouched base would contain neither. So the runner **merges a feat into the run's base the moment its `done` clears the verdict gate**, and every later worktree is cut from that updated base. The branch survives the merge; the worktree is removed. The human PR gate still sits where it always did, after the run, over the base branch — the session commits on its feat's branch and does not open one. - -Two things can send a finished feat back instead of landing it, both as partial work with a handoff rather than as failures. A **merge conflict** means a peer landed while this feat was in flight, so the next session rebases. **Uncommitted work** means the session produced every artifact but never committed: the verdict gate reads files, integration carries commits, and a feat recorded delivered on an empty merge would lose the work when its worktree is discarded. - -This makes git a **precondition**: `plan run` refuses a workspace that is not the root of a git repository, is on a detached HEAD, or has uncommitted changes (the run merges into that tree). Only the sessions overlap — the ledger, the journal and the run's bookkeeping are still written by a single writer, in the order sessions finish — and a squad run switches the live view to append-only lines, each tagged `[]`, because an in-place redraw from several sessions paints over itself. - -The run ends five ways: the plan **completes** (every feat is in the ledger), the **iteration cap** is hit (the wallet guard), the **stall guard** trips (default: 10 consecutive *failed* sessions — a session error, not a large feat mid-flight), the session **cannot be started at all** (5 consecutive spawn failures — a broken environment, not a broken plan), or a preflight refusal (unapproved or drifted plan). - -A run is **resumable**: re-running `csdd plan run ` after a crash or a Ctrl-C picks up where it stopped. Delivered feats come from the ledger and the spec tree; how many attempts each unfinished feat has spent, the handoff its last session left, and which feats already exhausted their bound are rebuilt from the append-only session record (`.csdd/plan//sessions.jsonl`) — so an interruption neither resets a feat's attempt budget nor throws away the handoff. +There is no runner. An approved plan is a **queue with a dependency graph**, and you +work it in your own session, one feat at a time: ```bash -csdd plan list # every plan: approval state + delivered/total feats (--json) -csdd plan status # feats, milestones, what's next -csdd plan run # bypass-mode loop — alerts + asks if the sandbox isn't verified -csdd plan run --yes # pre-accept the unverified-sandbox alert (non-interactive) - --session-budget 5 # per-session USD cap · --max-iterations (100) · --stall (10) - --squad-limit 3 # 3 sessions at once, one git worktree each (default 1: serial, in this checkout) - --enrich-model none # skip the per-feat context pass (default: sonnet) -csdd plan brief --feat F # print what the session gets (enriches on first use) -csdd plan brief --feat F --refresh # discard the stored context pack and run the pass again +csdd plan status # milestones, delivered, what is ready +csdd plan next # the feat whose Depends are all satisfied +csdd plan brief --feat # that feat's context — the mission ``` -#### When a run ends without completing +The **`plan-dev` skill** is the method: create the spec, drive `requirements → design +→ tasks` with the `spec-author` sub-agent (you review, the human approves each phase), +delegate every leaf task to `implementer` under the spec's `development_flow`, run the +Tier-3 gate through `quality-gate` plus `code-reviewer` at feat exit, and +`/csdd-commit` the slices. One feat per pass — the next one is your call, not a loop's. + +The **phase gates stay human**. `csdd plan approve` authorizes the *plan* — which feats +exist and under what constraints — not the specs you write from it. -There are no block markers to clear. Just `csdd plan run ` again — it resumes from the ledger, re-handing whatever feat is not yet marked done, and the first session on a previously-failed feat is pointed at its failure log under `.csdd/plan//failures/.log` (every attempt's untruncated output). `docs/plans//log.md` journals every feat outcome for the audit trail. +> **Where the autonomous loop went.** `csdd plan run` drove this headlessly — a session +> per feat, a verdict gate, worktrees, an attempt budget. Measured against the same work +> done in-session it cost an order of magnitude more and often delivered nothing: every +> session was rebuilt from zero per feat *and* per attempt, so all discovery was re-paid +> at orchestrator prices, and a `done` the gate refused threw away a session already paid +> for. The command now refuses with a pointer here. The runner code stays in the tree — +> the idea may come back, and it should come back as a revert, not a rewrite. -### 🔒 `sandbox` — isolation before autonomy +### 🔒 `sandbox` — isolation before bypass mode -`plan run` always drives Claude in bypass mode (`--dangerously-skip-permissions`), so it wants a **proven** sandbox: when `sandbox doctor` fails, the runner shows the failing checks and asks before continuing — declining closes the run. +Any bypass-mode work (`--dangerously-skip-permissions`) wants a **proven** sandbox rather than a trusted one: `csdd sandbox doctor` runs the checks and fails loudly when isolation cannot be proven. ```bash csdd sandbox init --hardened # scaffold a default-deny-egress devcontainer (--feature, --allow-domain) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index d3fbc38..8931a1b 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -149,7 +149,7 @@ RESOURCES destroy Tear the workspace back down (.claude/, CLAUDE.md, .mcp.json, pre-push); keeps specs/. Asks to confirm; --force to skip. steering {init,create,list,show,delete,validate} spec {init,list,show,status,generate,approve,validate,test-report,delete} - plan {init,list,validate,approve,status,next,brief,generate,run,unblock} Plans decompose an initiative into feats (docs/plans//); each feat becomes one spec. + plan {init,list,validate,approve,status,next,brief,generate} Plans decompose an initiative into feats (docs/plans//); each feat becomes one spec. sandbox {init,doctor} Scaffold a hardened default-deny-egress devcontainer; doctor proves isolation before bypass mode. skill {create,list,show,add-reference,add-script,add-asset,validate,delete} agent {create,list,show,delete} @@ -159,7 +159,7 @@ RESOURCES codewiki {lint} The repo-derived wiki document under docs/raw/ (citation + structure lint). export {kiro,codex} Convert the workspace to Kiro / Codex format. web Launch a read-only web dashboard (live spec progress + file viewer). - telegram {init,run} Relay plan-run progress + spec-status changes to a Telegram chat (read-only, outbound-only). + telegram {init,run} Relay spec-status changes + plan journals to a Telegram chat (read-only, outbound-only). GLOBAL FLAGS --root PATH Project root (default: nearest enclosing .claude/). @@ -202,7 +202,7 @@ EXAMPLES %[1]s web # serve the live dashboard; prints the local URL %[1]s web --tunnel # expose it publicly (pinggy by default; forces auth) %[1]s telegram init # save bot token + chat_id to .csdd/bot.json (gitignored) - %[1]s telegram run # relay plan-run + spec-status updates to Telegram + %[1]s telegram run # relay spec-status + plan journal updates to Telegram `, prog()) } diff --git a/internal/cli/plan.go b/internal/cli/plan.go index 7b62e2c..2efa189 100644 --- a/internal/cli/plan.go +++ b/internal/cli/plan.go @@ -1,7 +1,6 @@ package cli import ( - "context" "embed" "flag" "fmt" @@ -13,13 +12,12 @@ import ( "github.com/protonspy/csdd/internal/paths" "github.com/protonspy/csdd/internal/plan" "github.com/protonspy/csdd/internal/render" - "github.com/protonspy/csdd/internal/telegram" "github.com/protonspy/csdd/internal/templater" "github.com/protonspy/csdd/internal/workspace" ) -// runPlan dispatches `csdd plan ...`. M1 ships init/validate/status; -// approve/next/brief/generate/run land in later milestones (§5.8). +// runPlan dispatches `csdd plan ...`. `run` is answered by a refusal: +// the autonomous loop it drove is discontinued (see planRun). func runPlan(args []string, templates embed.FS) int { action, rest, err := parseAction("plan", args) if err != nil { @@ -48,7 +46,7 @@ func runPlan(args []string, templates embed.FS) int { case "generate": return planGenerate(rest, templates) case "run": - return planRun(rest, templates) + return planRun(rest) case "cost": return planCost(rest) case "verify": @@ -59,138 +57,27 @@ func runPlan(args []string, templates embed.FS) int { } } -func planRun(args []string, templates embed.FS) int { - fs := flag.NewFlagSet("plan run", flag.ContinueOnError) - var root, model, effort, enrichModel string - var autonomous, assumeYes, noTelegram bool - var sessionBudget float64 - var maxIterations, stall, featAttempts, maxRetries, maxRepairs, squadLimit int - var sessionIdle time.Duration - addRoot(fs, &root) - fs.BoolVar(&assumeYes, "yes", false, "Skip the unverified-sandbox prompt: accept running --dangerously-skip-permissions even when `sandbox doctor` fails.") - fs.BoolVar(&noTelegram, "no-telegram", false, "Do not auto-start the Telegram notifier even when a bot is configured (.csdd/bot.json).") - fs.BoolVar(&autonomous, "autonomous", false, "Deprecated no-op: plan run always runs bypass-mode (--dangerously-skip-permissions).") - fs.StringVar(&model, "model", "opus", "Model the orchestrating session runs on (claude --model): sonnet|opus|haiku|fable or a full model ID. It reviews and decides the spec; spec authoring is delegated to the `spec-author` sub-agent (sonnet) and task implementation to the `implementer` sub-agent (each on its own, cheaper model). Empty inherits the ambient default.") - fs.StringVar(&effort, "effort", "medium", "Reasoning effort the orchestrating session runs at (claude --effort): low|medium|high|xhigh|max. Empty inherits the ambient default.") - fs.StringVar(&enrichModel, "enrich-model", "sonnet", "Model the per-feat context pass runs on (claude --model). Before each feat is dispatched it reads the worktree once and records what the feat touches, what governs it and what is already there, so the orchestrator does not rediscover it every attempt. `none` turns the pass off and briefs from the plan alone.") - fs.Float64Var(&sessionBudget, "session-budget", 0, "Per-session cap in USD (claude --max-budget-usd). Default 0 = no cap; the session runs under the Claude account's own limits.") - fs.IntVar(&maxIterations, "max-iterations", 30, "Sessions the run may spend; one iteration is one claude session.") - fs.IntVar(&stall, "stall", 10, "Stop early after this many consecutive sessions without a step advancing.") - fs.DurationVar(&sessionIdle, "session-idle", 0, "Kill a session that makes no progress — no event stream output and no CPU — for this long (default 15m). Not a time limit: real work of any duration keeps resetting it.") - fs.IntVar(&featAttempts, "feat-attempts", 0, "Stop handing out ONE feat after this many sessions and surface it as blocked (default 4). Bounds a feat whose `done` the verdict gate keeps refusing.") - fs.IntVar(&squadLimit, "squad-limit", 0, "Maximum claude sessions running at once (1..6, default 1). At 1 the run is serial and works in this checkout, where the environment your suite needs is already installed. Above 1 each feat gets its own git worktree — cut from and merged back into the run's base branch, which requires a clean repository, and which starts without anything git ignores (node_modules/, .venv/, build caches).") - fs.IntVar(&maxRetries, "max-retries", 0, "Deprecated no-op: each iteration is one session, and the next iteration is the retry.") - fs.IntVar(&maxRepairs, "max-repairs", 0, "Deprecated no-op: the self-correcting loop replaced repair sessions.") - positionals, err := parseFlags(fs, args) - if err != nil { - return failOnFlagParse(err) - } - if len(positionals) < 1 { - render.Err("usage: " + prog() + " plan run SLUG [--model M] [--effort E] [--yes] [--session-budget N] [--max-iterations N] [--stall N] [--feat-attempts N]") - return 1 - } - if err := validateEffort(effort); err != nil { - render.Err(err.Error()) - return 1 - } - // 0 means "unset" so the runner's default applies; anything else must land in - // 1..6. The ceiling is the widest topological wave a real plan admitted — past - // it a plan cannot use the concurrency, and the shared Claude account limit is - // consumed that much faster for nothing. - if squadLimit < 0 || squadLimit > planSquadLimitMax { - render.Err(fmt.Sprintf("--squad-limit must be between 1 and %d (got %d)", planSquadLimitMax, squadLimit)) - return 1 - } - enrichModel = normalizeEnrichModel(enrichModel) - if autonomous { - render.Warn("--autonomous is deprecated and now a no-op: plan run always runs bypass-mode") - } - if maxRetries != 0 || maxRepairs != 0 { - render.Warn("--max-retries/--max-repairs are deprecated no-ops: every failure feeds the next session, bounded by --max-iterations and --stall") - } - slug := positionals[0] - if err := workspace.SafeName(slug, "plan"); err != nil { - render.Err(err.Error()) - return 1 - } - r, err := workspace.Resolve(root) - if err != nil { - render.Err(err.Error()) - return 1 - } - // Auto-start the Telegram notifier for the life of the run when a bot is - // configured, so run progress reaches the chat without a separate - // `csdd telegram run`. No-op when unconfigured or suppressed with --no-telegram. - if !noTelegram { - stopTelegram := startPlanTelegram(r) - defer stopTelegram() - } - - sum, err := plan.Run(plan.RunOptions{ - Root: r, - Slug: slug, - AssumeYes: assumeYes, - SessionBudget: sessionBudget, - Model: model, - Effort: effort, - MaxIterations: maxIterations, - Stall: stall, - FeatAttempts: featAttempts, - SessionIdle: sessionIdle, - SquadLimit: squadLimit, - EnrichModel: enrichModel, - WorktreeEntry: planEntryDoc(templates), - Out: os.Stdout, - }) - if err != nil { - render.Err(err.Error()) - return 1 - } - // Surface the run outcome as a distinct exit code (R9.4/R9.7); the summary is - // already printed by the runner. - return sum.Outcome -} - -// planSquadLimitMax is the hard ceiling on --squad-limit. It is not arbitrary: the -// widest topological wave the evidence plan (`agency-telegram-platform`, 31 feats) -// admits is 6, so beyond it a plan's own Depends graph cannot supply the -// parallelism, and the only effect of a larger number would be to burn the shared -// Claude account limit faster. -const planSquadLimitMax = 6 - -// startPlanTelegram auto-starts the read-only Telegram notifier for the duration -// of a plan run when a bot is configured (.csdd/bot.json). The notifier polls the -// run journal (docs/plans//log.md) the runner appends to, so each feat's -// done/progress/failed line — and every spec approval the sessions make — reaches -// the chat live. It returns a stop function that cancels the notifier and lets it -// flush the run's final journal lines. When no bot is configured (or the config is -// invalid) it is a silent no-op: Telegram stays opt-in via `csdd telegram init`. -func startPlanTelegram(root string) func() { - cfg, err := telegram.Load(root) - if err != nil || cfg.Validate() != nil { - return func() {} - } - notifier := telegram.NewNotifier(telegram.Options{ - Root: root, - Client: telegram.NewClient(cfg.Token, cfg.ChatID, apiBase()), - Interval: time.Duration(cfg.IntervalSeconds) * time.Second, - }) - ctx, cancel := context.WithCancel(context.Background()) - done := make(chan struct{}) - go func() { - defer close(done) - _ = notifier.Run(ctx) - }() - render.Info("telegram bot configured — relaying plan-run status to chat " + cfg.ChatID) - return func() { - cancel() - // Bound the wait so a slow or unreachable Telegram never wedges the CLI - // after the run itself has finished. - select { - case <-done: - case <-time.After(6 * time.Second): - } - } +// planRun answers `csdd plan run`, which is discontinued. +// +// The command drove an autonomous loop: one headless `claude -p` session per feat, +// a verdict gate, an attempt budget, a worktree each. Measured against the same +// work done interactively it cost an order of magnitude more and frequently +// delivered nothing — a session was rebuilt from zero per feat AND per attempt, so +// every discovery was re-paid at orchestrator prices and a `done` the gate refused +// threw a whole paid session away. That is the design, not a defect in it. +// +// It is a refusal rather than an "unknown action" because a user who types this has +// a plan in front of them and needs to know where the work moved, not that they +// mistyped. The runner itself (internal/plan) is untouched and still tested: the +// idea is on standby, and reviving it should be a revert rather than a rewrite. +func planRun(args []string) int { + // The whole message goes to stderr: this command produces no stdout artifact, + // and a refusal's guidance belongs on the same stream as the refusal. + render.Err("`" + prog() + " plan run` is discontinued — a plan is delivered in your own session now.\n" + + " " + prog() + " plan next # which feat is ready\n" + + " " + prog() + " plan brief --feat F # that feat's mission\n" + + " Work it with the `plan-dev` skill, approving each spec phase yourself.") + return 1 } // requireWorkspaceMarker enforces R1.3: every plan command operates inside an @@ -320,7 +207,7 @@ func printPlanList(sums []plan.Summary) { switch { case s.Drift: // Drift outranks approval in the column: the approval exists but no - // longer binds the current plan.md, which is what `plan run` refuses on. + // longer binds the current plan.md, which is what `plan next` reports as drift. approval = "drift" drifted = true case s.Approved: @@ -336,7 +223,7 @@ func printPlanList(sums []plan.Summary) { fmt.Printf(" %-*s %-8s %-9s %s\n", maxName, s.Slug, approval, feats, s.Name) } if drifted { - render.Warn("drift: plan.md/seeds changed since approval — re-approve before running") + render.Warn("drift: plan.md/seeds changed since approval — re-approve before working from it") } } @@ -519,7 +406,7 @@ func planNext(args []string) int { // normalizeEnrichModel maps `none` — how a human spells "no context pass" — onto // the empty model every enrichment caller already reads as disabled. It is shared -// so `plan run` and `plan brief` cannot disagree about what `none` means: for a +// so every enrichment caller spells it the same way: for a // while only the runner honored it, and `plan brief --enrich-model none` spawned a // session for a model literally named "none". func normalizeEnrichModel(model string) string { @@ -572,18 +459,15 @@ func planBrief(args []string) int { // The discovered half is not an extra. Without it the brief is the plan restated // — the feat row a human already wrote — and the session rediscovers the tree at // orchestrator prices, which is the whole cost the pass exists to avoid. So the - // pass runs BY DEFAULT here, exactly as it does before `plan run` dispatches a - // feat. `--enrich-model none` is how a caller that must not spawn a model — CI, a - // scripted diff — opts out. + // pass runs BY DEFAULT here. `--enrich-model none` is how a caller that must not + // spawn a model — CI, a scripted diff — opts out. // // A pack already on disk is kept, and that is a stronger rule here than in the // runner: it is reused even when the feat's row has moved on since it was // written. Regenerating costs a model call, briefing is something a human does // repeatedly while editing a plan, and re-spending on every one of those edits is // not a default anybody would choose. A stale pack costs a line on stderr and - // `--refresh` is the way to replace it. (`plan run` keeps invalidating on a - // changed row: what it dispatches a session to build has to match what it briefed - // that session with.) + // `--refresh` is the way to replace it. // // The pass runs against the workspace itself rather than a worktree: there is no // run in flight to have cut one, and the tree a human is looking at is the tree @@ -635,7 +519,7 @@ func planGenerate(args []string, templates embed.FS) int { var force, requireApproved bool addRoot(fs, &root) addForce(fs, &force) - fs.BoolVar(&requireApproved, "require-approved", false, "Fail unless the plan is approved and drift-free (used by `plan run`).") + fs.BoolVar(&requireApproved, "require-approved", false, "Fail unless the plan is approved and drift-free.") positionals, err := parseFlags(fs, args) if err != nil { return failOnFlagParse(err) @@ -965,16 +849,3 @@ func thousands(n int) string { } return b.String() } - -// planEntryDoc is the lean CLAUDE.md each feat's worktree gets for the duration of -// the run. A template that will not render is not worth failing a run over: the -// sessions fall back to the repository's own file, which is more expensive to read -// and still correct enough to work from. -func planEntryDoc(templates embed.FS) string { - doc, err := templater.PlanEntry(templates) - if err != nil { - render.Warn("could not load the plan-session CLAUDE.md; sessions will read the repository's own: " + err.Error()) - return "" - } - return doc -} diff --git a/internal/cli/plan_bridge_cli_test.go b/internal/cli/plan_bridge_cli_test.go index 417679e..ce63535 100644 --- a/internal/cli/plan_bridge_cli_test.go +++ b/internal/cli/plan_bridge_cli_test.go @@ -124,21 +124,27 @@ func TestPlanGenerateRequireApprovedFails(t *testing.T) { } } -func TestPlanRunPreflightCLI(t *testing.T) { - dir := freshWorkspace(t) - if code, _, e := run(t, "plan", "init", "photos", "--root", dir); code != 0 { - t.Fatalf("init failed: %s", e) - } - if err := os.WriteFile(filepath.Join(dir, "docs", "plans", "photos", "plan.md"), []byte(authoredPlan), 0o644); err != nil { - t.Fatal(err) +// TestPlanRunIsDiscontinued pins the refusal that replaced the autonomous loop. +// +// It refuses rather than reporting an unknown action because a user who types this +// has a plan in front of them: what they need is where the work moved, not that they +// mistyped. So the message is asserted for the path forward — the two commands that +// pick and brief a feat, and the skill that delivers it — and not merely for failing. +// It refuses on an APPROVED plan, which is the case that used to run: there is no +// preflight left to hide behind. +func TestPlanRunIsDiscontinued(t *testing.T) { + dir := scaffoldApprovedPlan(t) + code, out, errOut := run(t, "plan", "run", "photos", "--root", dir) + if code != 1 { + t.Errorf("plan run should exit 1, got %d (out=%q err=%q)", code, out, errOut) } - // Unapproved plan: run refuses before ever spawning a session. - code, _, errOut := run(t, "plan", "run", "photos", "--root", dir) - if code == 0 { - t.Errorf("plan run should refuse an unapproved plan") + if !strings.Contains(errOut, "discontinued") { + t.Errorf("the refusal should say the command is discontinued, got %q", errOut) } - if !strings.Contains(errOut, "not approved") { - t.Errorf("expected not-approved preflight error, got %q", errOut) + for _, want := range []string{"plan next", "plan brief", "plan-dev"} { + if !strings.Contains(errOut, want) { + t.Errorf("the refusal should point at %q, got %q", want, errOut) + } } } diff --git a/internal/cli/plan_cli_test.go b/internal/cli/plan_cli_test.go index 3d8853a..ff1ac5f 100644 --- a/internal/cli/plan_cli_test.go +++ b/internal/cli/plan_cli_test.go @@ -130,38 +130,24 @@ func TestPlanValidateJSONExitCode(t *testing.T) { } } -// TestPlanRunSquadLimitBounds pins the flag's contract. The ceiling is not -// arbitrary: 6 is the widest topological wave the evidence plan admits, so past it a -// plan's own Depends graph cannot supply the parallelism and a larger number would -// only consume the shared Claude account limit faster. -func TestPlanRunSquadLimitBounds(t *testing.T) { - dir := t.TempDir() - for _, n := range []string{"7", "99", "-2"} { - code, _, errOut := run(t, "plan", "run", "photos", "--root", dir, "--squad-limit", n) - if code == 0 { - t.Errorf("--squad-limit %s should be rejected", n) - } - if !strings.Contains(errOut, "must be between 1 and 6") { - t.Errorf("--squad-limit %s should name the bound, got %q", n, errOut) - } - } -} - // TestMisspelledFlagNamesItsNeighbour is a usability regression from a real report: -// `--squard-limit` produced only "flag provided but not defined", which reads as +// a one-key typo produced only "flag provided but not defined", which reads as // "that option does not exist" and got the whole capability written off as missing. +// It was reported against `--squard-limit`; that flag retired with `plan run`, so the +// guard rides on another flagged command — the behavior under test is the CLI's, not +// any one command's. func TestMisspelledFlagNamesItsNeighbour(t *testing.T) { dir := t.TempDir() - code, _, errOut := run(t, "plan", "run", "photos", "--root", dir, "--squard-limit", "2") + code, _, errOut := run(t, "plan", "brief", "photos", "--root", dir, "--enrich-modle", "sonnet") if code == 0 { t.Errorf("an undefined flag must still fail") } - if !strings.Contains(errOut, "did you mean --squad-limit?") { + if !strings.Contains(errOut, "did you mean --enrich-model?") { t.Errorf("a one-transposition typo should name the flag it meant, got %q", errOut) } // A name nothing is close to gets the stock message, not a guess. - _, _, errOut = run(t, "plan", "run", "photos", "--root", dir, "--wildly-unrelated", "2") + _, _, errOut = run(t, "plan", "brief", "photos", "--root", dir, "--wildly-unrelated", "2") if strings.Contains(errOut, "did you mean") { t.Errorf("a distant name must not be corrected into something else, got %q", errOut) } @@ -287,7 +273,7 @@ func TestPlanListShowsDrift(t *testing.T) { if !strings.Contains(out, "drift") { t.Errorf("the listing should flag the drifted approval: %s", out) } - if !strings.Contains(out+errOut, "re-approve before running") { + if !strings.Contains(out+errOut, "re-approve before working from it") { t.Errorf("drift should carry the fix: out=%q err=%q", out, errOut) } diff --git a/internal/cli/plan_skill_test.go b/internal/cli/plan_skill_test.go index a969fc9..43f1ea4 100644 --- a/internal/cli/plan_skill_test.go +++ b/internal/cli/plan_skill_test.go @@ -16,7 +16,6 @@ func TestPRDSkillAndCommandsInstalled(t *testing.T) { ".claude/commands/prd.md", ".claude/commands/csdd-plan-validate.md", ".claude/commands/csdd-plan-status.md", - ".claude/commands/csdd-plan-run.md", } for _, p := range mustExist { if _, err := os.Stat(filepath.Join(dir, p)); err != nil { diff --git a/internal/cli/plan_telegram_test.go b/internal/cli/plan_telegram_test.go deleted file mode 100644 index ba1f139..0000000 --- a/internal/cli/plan_telegram_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package cli - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - "sync" - "testing" - "time" - - "github.com/protonspy/csdd/internal/telegram" -) - -// TestStartPlanTelegramNoBotIsNoop: with no .csdd/bot.json, the auto-start is a -// silent no-op whose stop function is safe to call — a plan run without a bot is -// completely unaffected. -func TestStartPlanTelegramNoBotIsNoop(t *testing.T) { - stop := startPlanTelegram(t.TempDir()) - stop() // must not panic or block -} - -// TestStartPlanTelegramRelaysWhenConfigured: a configured bot makes the plan-run -// auto-start relay the run journal to the chat, and stop() flushes cleanly. -func TestStartPlanTelegramRelaysWhenConfigured(t *testing.T) { - root := t.TempDir() - if err := telegram.Save(root, telegram.Config{Token: "t", ChatID: "1"}); err != nil { - t.Fatal(err) - } - // A pre-existing run journal, so the notifier has a plan to watch. - logPath := filepath.Join(root, "docs", "plans", "ship-it", "log.md") - if err := os.MkdirAll(filepath.Dir(logPath), 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(logPath, []byte("## [old] seed | x | ok\n"), 0o644); err != nil { - t.Fatal(err) - } - - var mu sync.Mutex - var got []string - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var body struct { - Text string `json:"text"` - } - _ = json.NewDecoder(r.Body).Decode(&body) - mu.Lock() - got = append(got, body.Text) - mu.Unlock() - _, _ = w.Write([]byte(`{"ok":true}`)) - })) - defer srv.Close() - t.Setenv("TELEGRAM_API_BASE", srv.URL) - has := func(sub string) bool { - mu.Lock() - defer mu.Unlock() - for _, m := range got { - if strings.Contains(m, sub) { - return true - } - } - return false - } - - stop := startPlanTelegram(root) - // Wait for the startup banner, then append a run line and stop; the flush must - // relay it. - deadline := time.Now().Add(2 * time.Second) - for time.Now().Before(deadline) && !has("csdd telegram ligado") { - time.Sleep(10 * time.Millisecond) - } - f, err := os.OpenFile(logPath, os.O_APPEND|os.O_WRONLY, 0o644) - if err != nil { - t.Fatal(err) - } - _, _ = f.WriteString("## [2026-07-08] - | feat-a | done\n") - _ = f.Close() - stop() - - if !has("csdd telegram ligado") { - t.Errorf("expected the startup banner to be relayed; got %v", got) - } - if !has("feat-a") { - t.Errorf("expected the appended journal line to be flushed on stop; got %v", got) - } -} diff --git a/internal/cli/sandbox.go b/internal/cli/sandbox.go index 84ae050..928e266 100644 --- a/internal/cli/sandbox.go +++ b/internal/cli/sandbox.go @@ -43,7 +43,7 @@ func sandboxInit(args []string, templates embed.FS) int { addRoot(fs, &root) fs.Var(&features, "feature", "Add a toolchain devcontainer feature (repeatable): "+strings.Join(plan.SandboxFeatureNames(), "|")) fs.Var(&allowDomains, "allow-domain", "Add a host to the egress allow-list (repeatable).") - fs.BoolVar(&hardened, "hardened", false, "Restrict the vscode user's sudo to the firewall script (recommended for unattended `plan run`).") + fs.BoolVar(&hardened, "hardened", false, "Restrict the vscode user's sudo to the firewall script (recommended for unattended, bypass-mode agent work).") addForce(fs, &force) if _, err := parseFlags(fs, args); err != nil { return failOnFlagParse(err) diff --git a/internal/cli/spec.go b/internal/cli/spec.go index f69538c..08d8f34 100644 --- a/internal/cli/spec.go +++ b/internal/cli/spec.go @@ -590,7 +590,7 @@ func SpecApprove(opts SpecApproveOptions) error { if !state.Generated { // The generated flag is bookkeeping from `spec generate`; what an approval // certifies is the artifact content. A phase authored directly — by a human - // or a `plan run` session — is just as approvable, so only a missing + // or a session delivering a plan feat — is just as approvable, so only a missing // artifact blocks here. if !phaseAuthored(sdir, data, opts.Phase) { return fmt.Errorf("cannot approve '%s': %s not found — generate or author it first", opts.Phase, phaseArtifact(opts.Phase)) @@ -639,7 +639,7 @@ func SpecApprove(opts SpecApproveOptions) error { // phaseAuthored reports whether a phase has an artifact to validate or approve: // either `spec generate` marked it generated, or the artifact file itself exists -// (authored directly by a human or a plan-run session). Approval and validation +// (authored directly by a human or by a session working from a plan). Approval and validation // certify content, not the route it was produced by. func phaseAuthored(specDir string, data SpecJSON, phase string) bool { if data.Approvals[phase].Generated { diff --git a/internal/cli/telegram.go b/internal/cli/telegram.go index 03d375a..176c71c 100644 --- a/internal/cli/telegram.go +++ b/internal/cli/telegram.go @@ -23,7 +23,7 @@ import ( func apiBase() string { return strings.TrimSpace(os.Getenv("TELEGRAM_API_BASE")) } // runTelegram dispatches `csdd telegram {init,run}`: a read-only, outbound-only -// bridge that relays plan-run progress and spec-status changes to a Telegram +// bridge that relays spec-status changes and plan journals to a Telegram // chat. Like `csdd web`, it never authors artifacts — it is a view over the // workspace, delivered as messages. func runTelegram(args []string) int { diff --git a/internal/plan/brief.go b/internal/plan/brief.go index ca76a81..5c6bb59 100644 --- a/internal/plan/brief.go +++ b/internal/plan/brief.go @@ -11,15 +11,15 @@ import ( "github.com/protonspy/csdd/internal/paths" ) -// FeatBrief assembles the mission pack for one whole feat (R7): who is reading it, -// what this feat is, what governs it, what it is likely to touch, and what verifies -// it. It does NOT carry the development process — how a session authors and approves its spec -// phases, what it may delegate, what git it owns and what makes a `done` acceptable -// live in the plan-session CLAUDE.md the runner writes into every worktree and in -// the `plan-dev` skill, both of which the session already reads every turn. Stating -// them a second time here cost more than the duplication: the two copies had begun -// to disagree about the feat-exit checks, and a brief that argues against CLAUDE.md's -// interactive STOP rules is arguing against a document the worktree no longer has. +// FeatBrief assembles the context pack for one whole feat (R7): what this feat is, +// what governs it, what it is likely to touch, and what verifies it. +// +// It is CONTEXT and nothing else. It carries no role preamble, no development +// process, and no verdict protocol — those were written for the autonomous runner, +// which addressed a headless session that had this text and nothing else. A brief is +// read now by a session that already has its CLAUDE.md, its skills and a human in the +// room; telling it who it is and how to work would be restating, worse, what it is +// already governed by. What only the plan knows is the feat: that is what this holds. // // It draws on explicit content — the feat row, its seeds, the governing ADR/stack // refs (slugs/names/paths only — never ADR bodies, stack row details, or wiki @@ -30,9 +30,9 @@ import ( // rejects it in code), not by inlining their bodies (R7.2). // // It stays deterministic (R7.3): the pack is read from disk like the seeds are, so -// the same workspace state always renders the same brief. The autonomous run context -// (handoff, failure trail) is appended after this by the runner, so this prefix stays -// stable across a feat's sessions. +// the same workspace state always renders the same brief for the same workspace +// state — which is what makes it reviewable with `csdd plan brief` before anyone +// works from it. func FeatBrief(root string, doc *PlanDoc, feat Feat) (string, error) { if _, ok := doc.Feat(feat.Slug); !ok { return "", fmt.Errorf("feat %q is not in plan %q", feat.Slug, doc.Slug) @@ -40,23 +40,12 @@ func FeatBrief(root string, doc *PlanDoc, feat Feat) (string, error) { var b strings.Builder w := func(format string, a ...any) { fmt.Fprintf(&b, format, a...) } - // 0. Who is reading this. The brief is fed to `claude -p` on stdin as the entire - // prompt, so its opening lines are the only place the session's role is set — - // without them the first thing the model sees is a feat row, and it has to infer - // from context what it is supposed to be. It stays four lines on purpose: the - // process this engineer follows is NOT here (see the pointer at the end), and a - // preamble that grows is re-read on every turn of every session. - w("You are a senior software engineer. You deliver ONE feat of an approved plan to\n") - w("completion on your own, in an autonomous session with no human at the gate.\n") - w("Everything below is the mission: what this feat is, what governs it, where it\n") - w("lives in this repository, and what verifies it.\n\n") - // 1. The feat itself. w("# Feat: %s — plan %s\n\n", feat.Slug, doc.Slug) w("- Objective: %s\n", orDash(feat.Objective)) w("- Milestone: %s\n", orDash(feat.Milestone)) if len(feat.Depends) > 0 { - w("- Depends on (delivered earlier in this run): %s\n", strings.Join(feat.Depends, ", ")) + w("- Depends on (delivered earlier in this plan): %s\n", strings.Join(feat.Depends, ", ")) } w("\n") @@ -139,20 +128,11 @@ func FeatBrief(root string, doc *PlanDoc, feat Feat) (string, error) { // 7. The plan's own verification contract. w("## Quality gates for this plan\n\n") - w("Delegate these to the `quality-gate` sub-agent once, at feat exit:\n\n") + w("These must pass for this feat:\n\n") for _, g := range planGateCommands(doc) { w("- %s\n", g) } - // 8. Where the process lives. One pointer, not a copy: both documents are in the - // session's context already, and the copy is what drifted. - w("\n---\n\n") - w("Your authority, the cycle, delegation, git and the `done` gate are in this\n") - w("worktree's CLAUDE.md and the `plan-dev` skill — follow them, and do not stop\n") - w("for a human at a phase gate. Return `{\"status\":\"done\"}` when the whole feat is\n") - w("delivered, or `{\"status\":\"continue\"}` with the handoff for your successor in\n") - w("`summary`.\n") - return b.String(), nil } diff --git a/internal/plan/brief_test.go b/internal/plan/brief_test.go index f0d225a..095b099 100644 --- a/internal/plan/brief_test.go +++ b/internal/plan/brief_test.go @@ -124,13 +124,18 @@ func TestBriefCarriesNoProcess(t *testing.T) { t.Errorf("the brief carries process again (%q); that belongs in the plan-session CLAUDE.md:\n%s", banned, out) } } - // What it does carry instead: one pointer at where the process lives, and the - // verdict shape the runner parses. - if !strings.Contains(out, "plan-dev") || !strings.Contains(out, "CLAUDE.md") { - t.Errorf("the brief should point at the process rather than restate it:\n%s", out) + // Nor does it point AT the process any more. The pointer and the verdict shape + // addressed a headless session whose entire prompt was this text; a session that + // reads a brief now already has its CLAUDE.md and its skills, and there is no + // verdict to return to anybody. + for _, gone := range []string{"plan-dev", "CLAUDE.md", `{"status":"done"}`, "do not stop"} { + if strings.Contains(out, gone) { + t.Errorf("the brief still carries the runner-era process pointer (%q):\n%s", gone, out) + } } - if !strings.Contains(out, `{"status":"done"}`) { - t.Errorf("the brief should still name the verdict the runner parses:\n%s", out) + // And it opens on the feat, with no role preamble in front of it. + if !strings.HasPrefix(out, "# Feat: upload") { + t.Errorf("the brief should open on the feat itself:\n%s", firstLines(out, 6)) } } @@ -186,33 +191,6 @@ func TestBriefWithoutPackIsPlanOnly(t *testing.T) { } } -// TestBriefOpensByNamingTheRole pins the first four lines. -// -// The brief IS the prompt — it is fed to `claude -p` on stdin with nothing in front -// of it — so whatever it opens with is where the session's role is established. It -// used to open on a feat row, leaving the model to infer what it was supposed to be -// from a table of objectives. The preamble is deliberately short: it names the role -// and the shape of what follows, and stops. Process belongs to the plan-session -// CLAUDE.md and the `plan-dev` skill (see TestBriefCarriesNoProcess), so a preamble -// that starts explaining the cycle is this boundary eroding again. -func TestBriefOpensByNamingTheRole(t *testing.T) { - out := briefFor(t, setupWorkspace(t, "p", briefPlan), "p", "upload") - if !strings.HasPrefix(out, "You are a senior software engineer.") { - t.Errorf("the brief must open by naming the role it is written for:\n%s", firstLines(out, 6)) - } - // The role sits BEFORE the feat header — everything after it reads as material - // for that role rather than as a document that happened to arrive. - role, feat := strings.Index(out, "You are a senior software engineer"), strings.Index(out, "# Feat:") - if feat < 0 || role > feat { - t.Errorf("the role must come before the feat header:\n%s", firstLines(out, 8)) - } - // Four lines, then the mission. A preamble long enough to need a heading has - // stopped being a preamble. - if head, _, _ := strings.Cut(out, "# Feat:"); strings.Count(head, "\n") > 6 { - t.Errorf("the role preamble should stay short, got:\n%s", head) - } -} - // firstLines returns at most n lines of s, for readable failure output. func firstLines(s string, n int) string { lines := strings.Split(s, "\n") diff --git a/internal/templater/templater_test.go b/internal/templater/templater_test.go index 4711409..a3097ef 100644 --- a/internal/templater/templater_test.go +++ b/internal/templater/templater_test.go @@ -1,6 +1,7 @@ package templater import ( + "io/fs" "strings" "testing" "testing/fstest" @@ -637,3 +638,51 @@ func TestAgentEffortLadder(t *testing.T) { } } } + +// TestNoShippedArtifactMentionsPlanRun sweeps everything `csdd init` writes into a +// workspace for the retired command. +// +// `csdd plan run` drove an autonomous loop and is discontinued; the CLI answers it +// with a refusal. What makes that stick is not the refusal but this sweep: the +// command was named across a skill, three sandbox files, two PRD surfaces, an agent +// and the root CLAUDE.md, and an instruction that survives in a shipped artifact is +// an instruction a model will follow — it will read "run the loop" long after the +// loop stopped existing. A hit here means a reference crept back in an edit. +// +// The one deliberate exception is templates/plan/CLAUDE.md.tmpl, which is the +// runner's own entry document. The runner is on standby, not deleted, and that file +// is never installed into a workspace — nothing reads it unless the loop comes back. +func TestNoShippedArtifactMentionsPlanRun(t *testing.T) { + sets := map[string]func(fs.FS) (map[string]string, error){ + "agents": AgentFiles, + "skills": SkillFiles, + "rules": RuleFiles, + "commands": CommandFiles, + "workflow": WorkflowTemplateFiles, + } + for label, load := range sets { + files, err := load(FS) + if err != nil { + t.Fatalf("%s: %v", label, err) + } + for name, body := range files { + if strings.Contains(body, "plan run") { + t.Errorf("%s/%s still instructs `plan run`; the loop is discontinued", label, name) + } + } + } + root, err := Static(FS, "templates/root/CLAUDE.md.tmpl") + if err != nil { + t.Fatal(err) + } + if strings.Contains(root, "plan run") { + t.Error("the root CLAUDE.md still mentions `plan run`") + } + // The human gate is unconditional again: the exception that let a plan session + // approve its own spec phases existed only for the loop. + for _, gone := range []string{"sanctioned exception", "self-approves"} { + if strings.Contains(root, gone) { + t.Errorf("the root CLAUDE.md still carries the autonomous-loop carve-out (%q)", gone) + } + } +} diff --git a/internal/templater/templates/agents/quality-gate.md.tmpl b/internal/templater/templates/agents/quality-gate.md.tmpl index 2cf18c1..bdf6ede 100644 --- a/internal/templater/templates/agents/quality-gate.md.tmpl +++ b/internal/templater/templates/agents/quality-gate.md.tmpl @@ -43,11 +43,11 @@ npx -y @protonspy/csdd spec test-report --run --lang /test-report.json` to decide -whether a `done` verdict stands. A report that exists only as prose in your reply -leaves that file stale or absent, the verdict is rejected, and the whole feat is -handed back for another session. Saying "the suite is green" is not the same -artifact as the suite having been recorded green. +`specs//test-report.json` is the artifact a feat's completion is read +from — by `csdd spec status`, by the graph, and by whoever reviews the PR. A +report that exists only as prose in your reply leaves that file stale or absent, +so the feat reads as unverified however green the run actually was. Saying "the +suite is green" is not the same artifact as the suite having been recorded green. Do not narrow the command. A selector — `-k`, `--ignore`, `-x`, or a bare path like `tests/unit` — is recorded as an attention, and a report carrying an open diff --git a/internal/templater/templates/commands/csdd-plan-run.md.tmpl b/internal/templater/templates/commands/csdd-plan-run.md.tmpl deleted file mode 100644 index a8f3988..0000000 --- a/internal/templater/templates/commands/csdd-plan-run.md.tmpl +++ /dev/null @@ -1,59 +0,0 @@ ---- -description: Drive a csdd plan's autonomous loop — one Claude session per iteration, each handed one whole feat as its mission. The loop trusts the session's judgment and verifies the artifacts behind its verdict. Requires an approved, drift-free plan. -argument-hint: [--yes] -allowed-tools: Bash(csdd plan run:*), Bash(csdd plan status:*), Bash(csdd plan next:*), Bash(csdd plan brief:*), Bash(npx @protonspy/csdd plan:*) ---- - -## Task - -Drive the plan in `$ARGUMENTS` through `csdd plan run`. - -Preflight first: `csdd plan status ` must show the plan **approved** and -**drift-free**. Then: - -- `csdd plan run ` — the loop spawns one fresh session per iteration with - `--dangerously-skip-permissions` (default cap 30, `--max-iterations`). Each - session is handed ONE whole feat's mission brief and drives the entire flow - itself. Run it inside the hardened sandbox: the runner checks - `csdd sandbox doctor` first, and when isolation is NOT verified it alerts and - asks for confirmation — declining closes the run. -- `--yes` pre-accepts that alert (non-interactive runs); only pass it when the - human has explicitly accepted running bypass-mode outside a verified sandbox. -- When a Telegram bot is configured (`csdd telegram init` wrote `.csdd/bot.json`), - the run auto-starts the read-only notifier for its duration and relays each - feat's done/progress/failed line — plus every spec approval — to the chat; no - separate `csdd telegram run` is needed. Pass `--no-telegram` to suppress it. - -The runner is deliberately dumb: it hands out feats in plan order, spawns the -session, and records what the session declares — after checking that claim against -the artifacts on disk. **The session owns -everything else**: it authors and approves the spec (`csdd spec generate/validate/ -approve`), implements every task, runs the csdd dev-cycle, owns git (branch, -`/csdd-commit`, the pre-push gate, the PR), and records any open decision it makes -(docs/stack.md Decided row / ADR). The runner runs **no gates of its own** — it -trusts the session's judgment — but a `done` verdict is checked against the -artifacts it implies: every task box `[x]`, the spec approved and ready, and the -recorded test evidence green with no open attentions. A `done` those contradict is -converted to `continue` and handed back naming what failed. - -The verdict is one of two intents: - -- **`done`** — the whole feat is delivered and the session's own checks pass - (`csdd spec validate`, `csdd graph analyze --strict`, the plan's Quality Gates). - The runner records the feat in `.csdd/plan//progress.json` and moves on. -- **`continue`** — honest partial work: the same feat comes back next iteration - with the session's handoff carried forward. - -## When the run ends without completing - -The run ends four ways: the plan completes, the iteration cap is hit (the wallet -guard), the stall guard trips (`--stall`, default 10 consecutive *failed* sessions -— a session error, not a large feat mid-flight), or a preflight refusal. There are -no block markers to clear: just `csdd plan run ` again — it resumes from the -ledger, re-handing whatever feat is not yet marked done, and the first session on a -previously-failed feat is pointed at its failure log under -`.csdd/plan//failures/.log`. - -Audit an autonomous run through `docs/plans//log.md`: every feat outcome -(done / progress / failed) is journaled there — ratify the sessions' work by -reading it after the run. diff --git a/internal/templater/templates/commands/prd.md.tmpl b/internal/templater/templates/commands/prd.md.tmpl index 4b70728..77e50a3 100644 --- a/internal/templater/templates/commands/prd.md.tmpl +++ b/internal/templater/templates/commands/prd.md.tmpl @@ -20,8 +20,8 @@ real trade-off — each recorded the moment it resolves as `docs/adr/NNNN- `[DEFERRED-BY-HUMAN]`) — scaffolding with `csdd plan init` and running `csdd plan validate` after every authoring pass. -Author only. Do **not** run `csdd plan approve`, `csdd plan generate`, or -`csdd plan run`, and do not write `plan.json`, `docs/graph/`, or `.csdd/`. End by +Author only. Do **not** run `csdd plan approve` or `csdd plan generate`, and do +not write `plan.json`, `docs/graph/`, or `.csdd/`. End by showing the validated feat table and telling the user to run `csdd plan approve ` themselves. diff --git a/internal/templater/templates/plan/plan.md.tmpl b/internal/templater/templates/plan/plan.md.tmpl index 430168b..cb289b9 100644 --- a/internal/templater/templates/plan/plan.md.tmpl +++ b/internal/templater/templates/plan/plan.md.tmpl @@ -33,11 +33,10 @@ design → tasks → implementation. Columns (all required, matched by header na Depends comma-separated feat slugs that must be `done` first (— for none; range shorthand like `1-3` is a validation error — list slugs) Milestone grouping label (e.g. M1); feats run in milestone then table order - (P) ordering intent only — `P` if this feat MAY run alongside its - milestone siblings. It does NOT gate concurrency: `csdd plan run - --squad-limit N` schedules off the Depends graph alone, and each - concurrent feat works in its own git worktree, so feats never share - a working tree whether or not they are marked + (P) ordering intent only — `P` if this feat MAY be worked alongside its + milestone siblings. It does NOT schedule anything: the `Depends` + graph is what decides when a feat is ready, whether or not it is + marked Refs whitespace-separated [[wiki-page]], stack:, and adr: tokens; each stack: must match a docs/stack.md Decided row, each [[wiki-page]] a page under docs/wiki/pages/, and each diff --git a/internal/templater/templates/root/CLAUDE.md.tmpl b/internal/templater/templates/root/CLAUDE.md.tmpl index de21a0c..4812070 100644 --- a/internal/templater/templates/root/CLAUDE.md.tmpl +++ b/internal/templater/templates/root/CLAUDE.md.tmpl @@ -29,14 +29,6 @@ depends on: 5. **When a rule blocks you, stop and surface it** — report the blocked item to the human. Do not route around the gate. -**One sanctioned exception — the autonomous `csdd plan run` loop.** The human -authorized the whole plan up front (`csdd plan approve`), so a session there -self-approves its own *validated* spec phases (authoring delegated to the -`spec-author` sub-agent, each task to the `implementer`) and does not stop for a -human at a gate — rules 2, 4, and 5 govern *interactive* development. The -session still validates every phase before approving it. See the `plan-dev` -skill. - ## Driving csdd — the MCP tools are the default `csdd init` registers a **`csdd` MCP server** in `.mcp.json` — a stdio server @@ -89,7 +81,7 @@ npx @protonspy/csdd spec approve FEATURE --phase {requirements|design|tasks} [- npx @protonspy/csdd spec status FEATURE # show + validate npx @protonspy/csdd spec {list,show,validate,delete} FEATURE npx @protonspy/csdd graph {build,query,explain,path,analyze} … # knowledge graph -npx @protonspy/csdd plan {init,run,status,brief,validate,approve,cost,verify} … +npx @protonspy/csdd plan {init,list,status,next,brief,generate,validate,approve} … ``` You can also create **skills**, **custom sub-agents**, and register **MCP diff --git a/internal/templater/templates/root/knowledge-section.md.tmpl b/internal/templater/templates/root/knowledge-section.md.tmpl index fb03d11..0f4030d 100644 --- a/internal/templater/templates/root/knowledge-section.md.tmpl +++ b/internal/templater/templates/root/knowledge-section.md.tmpl @@ -66,12 +66,11 @@ an initiative into **feats**, each becoming exactly one spec. stack, decomposes into feats, and lints with `csdd plan validate` — ending at a validated plan for you to `csdd plan approve`. A single feature stays on the lighter `quick-prd` on-ramp. -- **Never edit an approved plan, and never execute approvals, during a run.** - `csdd plan run` is a dumb loop plus a verifier: it runs no gates itself, but - checks the artifacts behind a `done` verdict, and owns a per-feat ledger and - journals — while the session controls the whole flow: it - authors and approves the spec, runs the csdd dev-cycle, and owns git (branch, - `/csdd-commit`, the PR). Don't edit `plan.md`/`plan.json` — editing an approved +- **Never edit an approved plan while you are delivering from it.** An approved + plan is the contract the feat you are working was authorized against; `csdd` + owns `plan.json` and the state under `.csdd/`. You deliver a feat from it — + author its spec through the phase gates, run the csdd dev-cycle, own git + (branch, `/csdd-commit`, the PR). Don't edit `plan.md`/`plan.json` — editing an approved plan is real drift and stops autonomy until re-approval. - **The loop trusts the session's judgment, and verifies its artifacts.** A `done` verdict is accepted only when every task box is `[x]`, the spec is approved and diff --git a/internal/templater/templates/sandbox/Dockerfile.tmpl b/internal/templater/templates/sandbox/Dockerfile.tmpl index 0d8574d..0f53618 100644 --- a/internal/templater/templates/sandbox/Dockerfile.tmpl +++ b/internal/templater/templates/sandbox/Dockerfile.tmpl @@ -7,8 +7,8 @@ FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04 # HARDENED restricts the vscode user's sudo to ONLY the firewall script (no -# general root). It is the recommended build for unattended -# `csdd plan run`; the default keeps broad sudo for interactive use. +# general root). It is the recommended build for any unattended, bypass-mode +# agent work; the default keeps broad sudo for interactive use. ARG HARDENED=false # Firewall + resolver tooling. iptables/ipset enforce the default-deny egress; diff --git a/internal/templater/templates/sandbox/devcontainer.json.tmpl b/internal/templater/templates/sandbox/devcontainer.json.tmpl index 9cc2f3b..79ea73b 100644 --- a/internal/templater/templates/sandbox/devcontainer.json.tmpl +++ b/internal/templater/templates/sandbox/devcontainer.json.tmpl @@ -1,8 +1,8 @@ // csdd sandbox — devcontainer definition (JSONC; comments are allowed). // Open with your IDE's "Reopen in Container", or `devcontainer up`. Inside, -// bypass-mode Claude is safe: `csdd sandbox doctor` proves isolation and -// `csdd plan run` (always --dangerously-skip-permissions) alerts and requires -// an explicit accept before proceeding anywhere it can't prove it. +// bypass-mode Claude is safe: `csdd sandbox doctor` proves the isolation, so +// running with --dangerously-skip-permissions is a decision you can check +// rather than one you have to trust. { "name": "csdd-sandbox", "build": { diff --git a/internal/templater/templates/sandbox/init-firewall.sh.tmpl b/internal/templater/templates/sandbox/init-firewall.sh.tmpl index 8304389..80bfb9e 100644 --- a/internal/templater/templates/sandbox/init-firewall.sh.tmpl +++ b/internal/templater/templates/sandbox/init-firewall.sh.tmpl @@ -5,7 +5,7 @@ # ONLY those (plus loopback, DNS, and established return traffic); all other # outbound traffic is dropped. It then PROVES isolation: a control host must be # unreachable and api.anthropic.com reachable, or it exits non-zero so -# `csdd sandbox doctor` fails and `csdd plan run` alerts before proceeding. +# `csdd sandbox doctor` fails and the isolation is reported as unproven. set -euo pipefail ALLOWLIST="${1:-/etc/csdd/allowed-domains.txt}" diff --git a/internal/templater/templates/skills/plan-dev/SKILL.md.tmpl b/internal/templater/templates/skills/plan-dev/SKILL.md.tmpl index 17fb139..eb3869e 100644 --- a/internal/templater/templates/skills/plan-dev/SKILL.md.tmpl +++ b/internal/templater/templates/skills/plan-dev/SKILL.md.tmpl @@ -1,56 +1,54 @@ --- name: plan-dev -description: Use inside a `csdd plan run` session to deliver ONE approved-plan feat end-to-end — author + self-approve its spec phases, delegate each task to the `implementer` sub-agent (test-first), own git/PR, then return the done|continue verdict. This is the autonomous loop, so YOU are the approver; do not stop for a human at the phase gates. +description: Use to deliver ONE feat of an approved csdd plan end-to-end in this session — brief the feat, drive its spec through requirements/design/tasks with the `spec-author` sub-agent, delegate each task to `implementer` (test-first), gate it and commit. The human approves every phase; you never approve your own. --- -# Plan Dev — Autonomous Feat Delivery +# Plan Dev — Delivering One Feat ## Goal -Fully deliver **one** feat of an approved csdd plan in a single autonomous -`plan run` session: drive its spec through `requirements → design → tasks` -(you delegate authoring to the `spec-author` sub-agent, then review and decide), -delegate each task to the `implementer` sub-agent (per the spec's -`development_flow`), own the git dev-cycle and the PR, then return the verdict -schema. - -The loop trusts your **judgment** — it never reviews your code or re-runs your -suites — but it does check your **artifacts**. A `done` verdict passes through a -mechanical gate first: every task box `[x]` in `specs//tasks.md`, all three -phases approved with `ready_for_implementation` true, and -`specs//test-report.json` green with no open attentions. If any of those is -missing, your `done` is converted to `continue` and handed back with a note naming -what failed. So `done` means the whole feat is really delivered, not hoped — and -claiming it early costs you an attempt rather than shipping a broken feat. - -A feat that burns its attempt budget (default 4 sessions) is stopped and surfaced -for a human. Returning an honest `continue` is always cheaper than a `done` the -gate refuses. - -## Your authority — non-negotiable (this overrides CLAUDE.md's human gate) - -CLAUDE.md says phase approvals are a **human** gate and that you must STOP and -surface a blocked gate. That rule governs **interactive** development. **This is -the autonomous plan loop, and it is different:** - -- A human already opened the gate for this whole plan by running - `csdd plan approve `. **That IS the human authorization** the phase gates - require, and it covers every spec you author for this plan. -- So **you** approve this feat's spec phases yourself with - `csdd spec approve --phase …`. Do **not** pause for a human, do **not** - return `continue` just because a phase "needs approval", and do **not** treat - approving your own spec as routing around a gate — approving it is the mission. -- A blocked gate here means: **fix the artifact until it validates, then approve - and continue** — never escalate and stall. - -**Forbidden:** editing `plan.md` / `plan.json` (the approved plan is the -contract) or touching `.csdd/` (the loop's operational state, incl. -`progress.json`). +Deliver **one** feat of an approved csdd plan, end to end, in this session: take +its brief, drive its spec through `requirements → design → tasks` (you delegate +authoring to the `spec-author` sub-agent, then review and present each phase for +approval), delegate each task to the `implementer` sub-agent (per the spec's +`development_flow`), gate the result, and commit. + +**One feat per invocation.** When it is delivered you stop and report; picking the +next feat is a decision for the human, not a loop you run. + +## The feat, and its brief + +If you were not told which feat, ask csdd: + +```bash +csdd plan status # milestones, what is delivered, what is ready +csdd plan next # the feat whose Depends are all satisfied +csdd plan brief --feat # that feat's context — read this first +``` + +The brief is **context, not instructions**: the objective, the governing +`adr:`/`stack:`/`[[wiki]]` refs, the seeds, where the feat lives in this +repository, what is already there, what is missing, the pitfalls, and the plan's +quality gates. The method is this skill; the constraints are your CLAUDE.md. Fetch +a governor's detail with `csdd graph explain adr:` rather than assuming it. + +## The phase gates are the human's + +`csdd plan approve` authorized the **plan** — which feats exist, in what order, +under which constraints. It did not pre-approve the specs you are about to write. +Each phase is still a gate: + +- You **author and validate**; the human **approves**. Present the artifact, say + what you decided and what you left open, and stop there. +- Never run `csdd spec approve` yourself, and never `--force` past a gate. If a + phase will not validate, fix the artifact and present it again. +- **Forbidden:** editing `plan.md` / `plan.json` (the approved plan is the + contract) or writing under `.csdd/` (csdd's own state). ## Execution Workflow Work the phases in order. Each phase is **generate → review → validate → -self-approve**; the gate refuses the next phase until the current one is approved. +present**; the gate refuses the next phase until the human has approved this one. ### 0) Orient via the knowledge graph (before reading code) - `csdd graph query ` to find the nodes for the concept, then @@ -63,16 +61,17 @@ self-approve**; the gate refuses the next phase until the current one is approve `tdd`/`tdd-e2e` REQUIRED for money, auth, tenancy, anything irreversible. The flow decides the shape of `tasks.md`, and `csdd spec validate` enforces it. -### 2) Requirements — delegate to `spec-author`, then review and approve +### 2) Requirements — delegate to `spec-author`, then review and present - Dispatch the **`spec-author`** sub-agent with the feat + phase `requirements` + any governing refs. It scaffolds, consults the graph, authors the body (EARS, one behavior per criterion, numeric IDs), and runs `csdd spec validate`. - **REVIEW** the artifact: EARS phrasing, one behavior per criterion, numeric IDs. - Run `csdd spec validate ` yourself. Re-dispatch `spec-author` with a fix-list if the review or validate finds gaps; do not hand-edit inline. -- `csdd spec approve --phase requirements`. +- Present it and ask for `csdd spec approve --phase requirements`. + Wait for the approval before generating the next phase — the gate enforces it. -### 3) Design — delegate to `spec-author`, then review and approve +### 3) Design — delegate to `spec-author`, then review and present - Dispatch **`spec-author`** for phase `design` (add `--artifact research` first for a brownfield/novel area, and pass the governing ADRs/stack it must comply with). @@ -80,23 +79,23 @@ self-approve**; the gate refuses the next phase until the current one is approve `## File Structure Plan`, a `## Requirements Traceability` row for every requirement ID, and every technology choice inside a `docs/stack.md` Decided row with governing ADRs cited. -- `csdd spec validate ` → re-dispatch `spec-author` to fix gaps → - `csdd spec approve --phase design`. +- `csdd spec validate ` → re-dispatch `spec-author` to fix gaps → present + it for `csdd spec approve --phase design`. -### 4) Tasks — delegate to `spec-author`, then review and approve +### 4) Tasks — delegate to `spec-author`, then review and present - Dispatch **`spec-author`** for phase `tasks`. - **REVIEW** the artifact: every leaf task has `_Requirements:_`; every `(P)` task has a `_Boundary:_` matching a design component; `_Depends:_` IDs exist; no two `(P)` share a boundary; the shape matches the `development_flow` (`tdd`/`tdd-e2e`: RED→GREEN pairs; `unit`: implement → cover). -- `csdd spec validate ` → re-dispatch `spec-author` to fix gaps → - `csdd spec approve --phase tasks`. +- `csdd spec validate ` → re-dispatch `spec-author` to fix gaps → present + it for `csdd spec approve --phase tasks`. - `ready_for_implementation` flips to true only after all three approvals — and only csdd flips it. Never hand-edit `spec.json`. ### 5) Implement — delegate each task to the `implementer` sub-agent -You authored and approved the spec; those were the decisions and they ran on your -(orchestrator) model. Implementation is the *execution* of a decision already made, +The spec is authored and approved; those were the decisions and they ran on your +model. Implementation is the *execution* of a decision already made, so **delegate it** — do not hand-write task code inline. **Test timing — test the right thing at the right moment, not everything always.** @@ -179,7 +178,7 @@ can see them *combined*, which is the only failure Tier 2 structurally cannot ca `docs/stack.md` **Decided** row, plus a `docs/adr` record when the *why* needs more than a line. Prefer the option that deviates least from the decided stack. -### 8) Feat-exit self-check BEFORE you declare done +### 8) Feat-exit check BEFORE you report the feat delivered Run these **once**, here — not after every task. **Delegate the gate; do not run it inline.** Spawn the **`quality-gate`** @@ -207,9 +206,9 @@ You still check yourself, because these read state rather than produce output: - The `quality-gate` verdict is PASS, and `specs//test-report.json` is green with no open attentions. -These are the same artifacts the loop's verdict gate reads. If one of them fails -here, the gate will refuse your `done` anyway — fixing it now is strictly cheaper -than spending an attempt to be told. +These four are what "delivered" means for a feat, and they are what the reviewer +of the PR — and `csdd plan status` — will read. A feat reported delivered with one +of them red is a feat somebody else has to discover. ## Handling a blocked gate @@ -219,22 +218,27 @@ than spending an attempt to be told. approving it) → re-validate that phase, re-approve it, then continue; `--force` is only for clearing a stale prior-phase hash you have **just** re-validated, never for skipping review. -- **Out of room** before the feat is complete → return `continue` with a precise - handoff (what is done, what remains, what to try next) in `summary`. - -## Verdict protocol - -Return exactly one, as the JSON schema the runner gave you: -- `{"status":"done"}` — the WHOLE feat is delivered and every self-check in step - 8 passes. Never claim done on hope: the loop verifies the artifacts behind this - claim, and a refused `done` costs you one of the feat's attempts. -- `{"status":"continue","summary":""}` — honest partial work; the same - feat comes back next session with your handoff. +- **A phase that will not validate however you fix it** → stop and surface it with + the finding quoted and what you tried. A plan whose feat cannot be specified is + a plan to revise, and revising it is the human's call. +- **Out of room** before the feat is complete → stop and hand off in plain words: + what is done, what remains, what to try next. Leave the workspace committed and + the spec honest, so the next session starts from state rather than from guesses. + +## Reporting the feat + +There is no verdict schema and nothing parses your reply — you are reporting to a +human. Say, in plain words: what the feat delivers, which phases were approved, +what the feat-exit checks returned, every decision you recorded (stack row / ADR), +and anything you deliberately left open. If the feat is NOT complete, say so +first: an honest "3 of 7 tasks land, the composer is not started" is worth more +than a summary that has to be audited. ## Gotchas -- You are the approver here — stalling at a phase "waiting for a human" is the - single most common failure of this loop. Approve your own validated phases. +- The human approves each phase. Present the artifact and what you decided; do + not run `csdd spec approve` yourself and do not generate the next phase before + the gate opens. - You orchestrate; you do not draft or implement. Delegate spec authoring to the `spec-author` sub-agent (sonnet) and each task to the `implementer` sub-agent, then review their work. One task per implementer (one cycle pass); do not @@ -242,12 +246,12 @@ Return exactly one, as the JSON schema the runner gave you: drop the flow's test obligation — the RED test under `tdd`/`tdd-e2e`, the same-task test under `unit`. - Never hand-edit `spec.json`, frontmatter, or `.csdd/`; regenerate instead. -- Do not edit `plan.md`/`plan.json` — surface a genuinely wrong plan in your - `continue` summary instead, and keep working what you can. -- `done` is a promise the loop DOES verify — mechanically, against tasks.md, - spec.json, and test-report.json. If a self-check fails and you can't fix it this - session, return `continue`, not `done`: an honest handoff keeps the feat moving, - a refused `done` just burns an attempt to tell you what you already knew. +- Do not edit `plan.md`/`plan.json` — surface a genuinely wrong plan to the human + instead, and keep working what you can. +- "Delivered" is a claim somebody will check against `tasks.md`, `spec.json` and + `test-report.json`. If a feat-exit check fails and you cannot fix it now, report + that plainly — a precise partial is worth more than a delivered-shaped summary + that the next reader has to take apart. - Do not re-verify what a sub-agent already verified. Its report is the evidence for its task; your gates run once, at batch merge and at feat exit. @@ -257,13 +261,12 @@ Return exactly one, as the JSON schema the runner gave you: and every plan Quality Gate command. - Check: all three phases approved, `ready_for_implementation` true, every task box `[x]`, `specs//test-report.json` green with no open attentions, and the - PR opened. -- If any fails and you cannot resolve it now: return `continue` with a handoff — - do **not** return `done`. + work committed. +- If any fails and you cannot resolve it now: say which, and what you tried. ## Completion Criteria -- [ ] `requirements`, `design`, `tasks` each generated, validated, and - self-approved (`csdd spec approve … --phase …`). +- [ ] `requirements`, `design`, `tasks` each generated, validated, presented, and + approved by the human (`csdd spec approve … --phase …`). - [ ] Every leaf task delivered by an `implementer` sub-agent running the flow's cycle (`tdd-cycle` or `unit-cycle`) — or by a fallback pass of that same skill — and its box `[x]`. @@ -273,5 +276,5 @@ Return exactly one, as the JSON schema the runner gave you: - [ ] `specs//test-report.json` is green with no open attentions. - [ ] Any new technology / hard-to-reverse decision recorded (stack row / ADR). - [ ] Branch, commits via `/csdd-commit`, and the PR are done. -- [ ] Verdict returned: `done` only if all the above hold, else `continue` with a - handoff. +- [ ] Reported: what the feat delivers, what the exit checks returned, what was + recorded, and anything left open. diff --git a/internal/templater/templates/skills/prd/SKILL.md.tmpl b/internal/templater/templates/skills/prd/SKILL.md.tmpl index 7b2d6fa..da91c1f 100644 --- a/internal/templater/templates/skills/prd/SKILL.md.tmpl +++ b/internal/templater/templates/skills/prd/SKILL.md.tmpl @@ -28,8 +28,8 @@ ends by handing a lint-clean plan to the human for `csdd plan approve`. a milestone-sized effort, anything a single `requirements.md` can't hold). - **Not** for one feature — use `quick-prd` (its `docs/product/prd-.md` is an accepted *input* to this skill's Draft step). -- **Not** to execute the plan — that is `csdd plan run` (bypass-mode, ideally - inside the hardened sandbox), which this skill never triggers. +- **Not** to execute the plan — delivering a feat from an approved plan is the + `plan-dev` skill's job, which this skill never triggers. ## Collect Inputs First @@ -146,9 +146,9 @@ approved plan and its decisions is always a human-gated act, never done mid-run. ## Gotchas -- **Author only.** Never run `csdd plan approve`, `csdd plan generate`, or - `csdd plan run`; never write `plan.json`, `docs/graph/`, or `.csdd/`. The plan - is presented for the human to approve. +- **Author only.** Never run `csdd plan approve` or `csdd plan generate`; never + write `plan.json`, `docs/graph/`, or `.csdd/`. The plan is presented for the + human to approve. - **Range shorthand in Depends is a lint error** — list feat slugs explicitly. - **Undeclared tech and broken `[[refs]]` fail `plan validate`** — do steps 2–3 before wiring them into the table.