Skip to content

feat(plan): cut plan-run token cost with a spec-author sub-agent and a leaner brief - #87

Merged
protonspy merged 3 commits into
mainfrom
feat/plan-run-cost-reduction
Jul 28, 2026
Merged

feat(plan): cut plan-run token cost with a spec-author sub-agent and a leaner brief#87
protonspy merged 3 commits into
mainfrom
feat/plan-run-cost-reduction

Conversation

@protonspy

@protonspy protonspy commented Jul 28, 2026

Copy link
Copy Markdown
Owner

What

A real csdd plan run burned ~$200 in record time. The cost was the methodology itself: every feat paid, on the opus orchestrator, for full spec authoring, graph consultation, and gates before any code. This moves the bulk to cheaper models and trims what each feat re-reads, while keeping EARS, traceability, and phase gates — and adds the mechanical stack/ADR conformance the inlined brief was the only proxy for.

Changes

  • spec-author sub-agent (sonnet) authors each spec phase; opus only reviews, validates, and approves, re-dispatching spec-author with a fix-list. Authoring is the bulk of the per-feat spend; it does not need the orchestrator's model.
  • FeatBrief trimmed to the feat and its specs only. Stack/ADR/wiki are refs (bare slugs/names/paths) — bodies, versions, and descriptions are no longer inlined; the session fetches them via csdd graph explain adr:<slug> / csdd graph query when it needs them. The brief is re-read every turn, so this is the highest-leverage cut.
  • Default development_flowunit (tdd/tdd-e2e required for money, auth, tenancy, anything irreversible). spec generate --artifact tasks is now flow-aware: a unit-flow spec scaffolds from tasks-unit.md.tmpl (implement→cover, no RED/GREEN titles), so it passes checkDevelopmentFlow instead of fighting it.
  • Mechanical stack/ADR conformance (the backstop that makes trimming the brief safe — previously no mechanical check existed): plan validate now requires design.md to cite each governing adr:<slug> the feat's Refs declare (the brief no longer inlines the bodies, so the design must reference each governor); the code-reviewer sub-agent checks the implementation against the docs/stack.md Decided rows.
  • plan-dev reinforces test timing: Tier-2 (the implementer, --fast) tests the part during the task; Tier-3 (the quality-gate, coverage on) tests the whole at batch-exit and feat-exit. Never run the full suite inline in the orchestrator context.
  • Root CLAUDE.md trimmed and reframed MCP-first: the csdd_* MCP tools are the default path (a stdio server running the csdd binary; typed params carry the artifact/phase contract); the CLI block is the canonical reference/fallback. Removed the skill/agent/MCP authoring instructions in favor of a pointer to csdd --help. unit is the documented default.

Verification

  • go build ./..., go vet ./internal/..., gofmt clean.
  • go test -race ./internal/... all green.
  • Smoke: csdd init scaffolds spec-author.md; default flow = unit; explicit tdd works; flow-aware tasks scaffold (unit: no RED/GREEN, tdd: RED/GREEN); plan validate catches a design not citing a governing adr: and clears when cited.

Notes

  • The root CLAUDE.md.tmpl is a load-bearing static content file; review welcome on the wording/trim.
  • Keeps --model opus (orchestrator), --session-budget 0 (no cap), --max-iterations 30, --feat-attempts 4.

Summary by CodeRabbit

  • New Features
    • Added plan cost reporting (totals, per-feature, and optional per-model breakdown).
    • Added plan verify to validate delivery evidence and flag inconsistencies.
    • Added spec-author templating and unit-flow task templates.
  • Improvements
    • Default development flow is now unit (including validation and task structure).
    • Plan briefs now reference governing decisions/stack without inlining their full content.
    • Plan run defaults adjusted (lower effort/iteration/attempt caps) and worktree CLAUDE.md is injected.
  • Bug Fixes
    • Merge conflicts limited to generated artifacts no longer block delivery.

…a leaner brief

A real `csdd plan run` burned ~$200 in record time. The cost was the
methodology itself: every feat paid, on the opus orchestrator, for full spec
authoring, graph consultation, and gates before any code. This moves the
bulk to cheaper models and trims what each feat re-reads, while keeping
EARS, traceability, and phase gates — and adds the mechanical stack/ADR
conformance the inlined brief was the only proxy for.

- spec-author sub-agent (sonnet) authors each spec phase; opus only reviews,
  validates, and approves, re-dispatching spec-author with a fix-list
- FeatBrief keeps only the feat and its specs: stack/ADR/wiki are refs only,
  fetched via `csdd graph explain`/`graph query`; bodies no longer inlined
- default development_flow -> unit (tdd required for money/auth/tenancy);
  `spec generate --artifact tasks` is flow-aware (unit-shaped template)
- mechanical stack/ADR conformance: `plan validate` requires design.md to
  cite each governing adr:<slug>; the code-reviewer checks the implementation
  against the decided stack
- plan-dev reinforces test timing: Tier-2 tests the part during the task,
  Tier-3 tests the whole at feat exit; never the full suite inline
- root CLAUDE.md trimmed and reframed MCP-first (the csdd_* tools are the
  default; typed params carry the artifact/phase contract)
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@protonspy, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 44 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b770d8f7-e67d-4baf-8b7a-4d3bf6050d9a

📥 Commits

Reviewing files that changed from the base of the PR and between fa67b3e and c062c8e.

📒 Files selected for processing (30)
  • internal/cli/cli_extra_test.go
  • internal/cli/plan.go
  • internal/cli/plan_adr_test.go
  • internal/cli/spec.go
  • internal/cli/spec_flow_test.go
  • internal/plan/adr_test.go
  • internal/plan/brief.go
  • internal/plan/brief_test.go
  • internal/plan/cost.go
  • internal/plan/cost_verify_test.go
  • internal/plan/debug_test.go
  • internal/plan/ledger.go
  • internal/plan/metrics.go
  • internal/plan/runner.go
  • internal/plan/tree.go
  • internal/plan/validate.go
  • internal/plan/validate_test.go
  • internal/plan/verify.go
  • internal/templater/templater.go
  • internal/templater/templater_test.go
  • internal/templater/templates/agents/code-reviewer.md.tmpl
  • internal/templater/templates/agents/spec-author.md.tmpl
  • internal/templater/templates/commands/csdd-plan-run.md.tmpl
  • internal/templater/templates/plan/CLAUDE.md.tmpl
  • internal/templater/templates/root/CLAUDE.md.tmpl
  • internal/templater/templates/skills/plan-dev/SKILL.md.tmpl
  • internal/templater/templates/spec/tasks-unit.md.tmpl
  • internal/templater/templates/spec/tasks.md.tmpl
  • internal/validator/validator.go
  • internal/validator/validator_test.go
📝 Walkthrough

Walkthrough

The change adds plan cost and delivery verification, per-model session accounting, worktree entry injection, generated-artifact merge handling, refs-only governance briefs, ADR/design validation, and unit-flow defaults with flow-specific task generation.

Changes

Plan execution and delivery evidence

Layer / File(s) Summary
Session metrics and cost aggregation
internal/plan/metrics.go, internal/plan/ledger.go, internal/plan/cost.go, internal/cli/plan.go, internal/plan/cost_verify_test.go
Session usage is recorded by model, aggregated into cost reports, and exposed through console and JSON plan cost output.
Delivery verification command
internal/plan/verify.go, internal/cli/plan.go, internal/plan/cost_verify_test.go
plan verify compares ledger, artifacts, merge history, and worktree state, reporting feat states and findings.
Runner worktrees and merge integration
internal/plan/runner.go, internal/plan/tree.go, internal/templater/templater.go, internal/cli/plan.go, internal/plan/debug_test.go, internal/plan/cost_verify_test.go
Runner options inject a per-worktree entry document, while generated-only conflicts are auto-resolved and authored conflicts are rolled back.
Autonomous plan execution contracts
internal/templater/templates/agents/*, internal/templater/templates/plan/*, internal/templater/templates/root/*, internal/templater/templates/skills/plan-dev/*, internal/templater/templates/commands/*
Templates define delegated spec authoring, implementation, validation, approval, flow selection, and bounded plan-run behavior.

Unit development flow

Layer / File(s) Summary
Unit flow defaults and task generation
internal/cli/spec.go, internal/validator/validator.go, internal/templater/templates/spec/tasks.md.tmpl
Missing development-flow values now resolve to unit, and unit-flow task generation selects the unit template.
Unit flow contracts and regression tests
internal/templater/templates/spec/tasks-unit.md.tmpl, internal/cli/*_test.go, internal/validator/validator_test.go
Unit task phases and annotations are defined, with tests covering default resolution, validation, and tdd/unit task shapes.

Governance references and design validation

Layer / File(s) Summary
Refs-only brief generation
internal/plan/brief.go, internal/plan/*_test.go, internal/cli/plan_adr_test.go
Generated briefs list stack, ADR, and wiki references without inlining their resolved content.
Design ADR citation validation
internal/plan/validate.go, internal/plan/validate_test.go
Plan validation checks that authored designs cite governing ADR slugs when those references are valid.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant PlanCLI as plan CLI
  participant Runner as plan runner
  participant Worktree as feat worktree
  participant Ledger as session ledger
  participant Verify as delivery verifier
  Operator->>PlanCLI: run plan
  PlanCLI->>Runner: start bounded execution
  Runner->>Worktree: prepare entry document and execute feat
  Runner->>Ledger: record session metrics and verdict
  Operator->>PlanCLI: request cost or verification report
  PlanCLI->>Ledger: load session evidence
  PlanCLI->>Verify: inspect artifacts, merges, and worktrees
  Verify->>PlanCLI: return report and findings
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: plan-run cost reduction via a spec-author sub-agent and a leaner brief.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/plan-run-cost-reduction

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
internal/plan/metrics.go (1)

134-158: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two unmarshals of the same entry.

modelTokens and modelCost each decode raw independently; one decode into a shared map would be simpler and cheaper.

♻️ Suggested consolidation
-func modelTokens(raw json.RawMessage) SessionTokens {
-	var m map[string]any
-	if json.Unmarshal(raw, &m) != nil {
-		return SessionTokens{}
-	}
-	return SessionTokens{
+func modelUsage(raw json.RawMessage) (SessionTokens, float64) {
+	var m map[string]any
+	if json.Unmarshal(raw, &m) != nil {
+		return SessionTokens{}, 0
+	}
+	t := SessionTokens{
 		Input:         pickNum(m, modelUsageKeys["input"]),
 		Output:        pickNum(m, modelUsageKeys["output"]),
 		CacheRead:     pickNum(m, modelUsageKeys["cache_read"]),
 		CacheCreation: pickNum(m, modelUsageKeys["cache_creation"]),
 	}
-}
-
-func modelCost(raw json.RawMessage) float64 {
-	var m map[string]any
-	if json.Unmarshal(raw, &m) != nil {
-		return 0
-	}
 	for _, k := range modelUsageKeys["cost"] {
 		if f, ok := m[k].(float64); ok {
-			return f
+			return t, f
 		}
 	}
-	return 0
+	return t, 0
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/plan/metrics.go` around lines 134 - 158, Consolidate the duplicate
JSON decoding performed by modelTokens and modelCost into a single decode of
each raw entry, then reuse the resulting map for both token and cost extraction.
Preserve the existing zero-value behavior when unmarshalling fails and retain
the current key-selection logic in pickNum and modelUsageKeys.
internal/cli/plan.go (1)

71-77: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Help text and defaults disagree for --effort.

Line 72 sets the default to "medium" but says "Empty inherits the ambient default" — the flag can never be empty unless explicitly passed --effort="", and RunOptions docs (runner.go Line 92) still claim the CLI defaults to "opus / high". Worth reconciling the runner comment with the new medium default.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/plan.go` around lines 71 - 77, The --effort default and its
documented behavior are inconsistent. Update the `effort` flag definition and
the related `RunOptions` documentation in `runner.go` so the stated defaults and
empty-value inheritance behavior accurately match the implementation, including
reconciling the documented orchestrator model/effort defaults with the current
CLI values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/spec.go`:
- Around line 492-494: Update effectiveFlow to trim surrounding whitespace from
development_flow before determining the effective flow, keeping template
selection in SpecGenerate consistent with validator.specDevelopmentFlow. Add a
regression test covering a whitespace-only development_flow and verify it
selects the unit tasks template rather than the TDD template.

In `@internal/plan/brief.go`:
- Around line 111-128: Update the governing-decisions text in the brief
generation block to reference “csdd plan validate” instead of “csdd spec
validate,” including the self-check guidance for ADR citations. Keep the
existing ADR resolution and design citation requirements unchanged.

In `@internal/plan/cost.go`:
- Around line 52-54: Remove the unused FeatCost.wasted method from the cost
reporting code; callers can use the exported FeatCost.Gated field directly. Do
not add replacement logic unless an existing report specifically requires the
helper.

In `@internal/templater/templates/spec/tasks-unit.md.tmpl`:
- Around line 16-18: Update the development-flow guidance in the spec template
to identify both “tdd” and “tdd-e2e” as supported RED/GREEN alternatives,
phrased as “tdd or tdd-e2e, as appropriate.” Preserve the instruction to set
development_flow and regenerate tasks rather than manually shaping RED/GREEN
tasks.

---

Nitpick comments:
In `@internal/cli/plan.go`:
- Around line 71-77: The --effort default and its documented behavior are
inconsistent. Update the `effort` flag definition and the related `RunOptions`
documentation in `runner.go` so the stated defaults and empty-value inheritance
behavior accurately match the implementation, including reconciling the
documented orchestrator model/effort defaults with the current CLI values.

In `@internal/plan/metrics.go`:
- Around line 134-158: Consolidate the duplicate JSON decoding performed by
modelTokens and modelCost into a single decode of each raw entry, then reuse the
resulting map for both token and cost extraction. Preserve the existing
zero-value behavior when unmarshalling fails and retain the current
key-selection logic in pickNum and modelUsageKeys.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 195cc288-42cb-4e18-89c0-cafbcc4d245b

📥 Commits

Reviewing files that changed from the base of the PR and between fa67b3e and 115b705.

📒 Files selected for processing (30)
  • internal/cli/cli_extra_test.go
  • internal/cli/plan.go
  • internal/cli/plan_adr_test.go
  • internal/cli/spec.go
  • internal/cli/spec_flow_test.go
  • internal/plan/adr_test.go
  • internal/plan/brief.go
  • internal/plan/brief_test.go
  • internal/plan/cost.go
  • internal/plan/cost_verify_test.go
  • internal/plan/debug_test.go
  • internal/plan/ledger.go
  • internal/plan/metrics.go
  • internal/plan/runner.go
  • internal/plan/tree.go
  • internal/plan/validate.go
  • internal/plan/validate_test.go
  • internal/plan/verify.go
  • internal/templater/templater.go
  • internal/templater/templater_test.go
  • internal/templater/templates/agents/code-reviewer.md.tmpl
  • internal/templater/templates/agents/spec-author.md.tmpl
  • internal/templater/templates/commands/csdd-plan-run.md.tmpl
  • internal/templater/templates/plan/CLAUDE.md.tmpl
  • internal/templater/templates/root/CLAUDE.md.tmpl
  • internal/templater/templates/skills/plan-dev/SKILL.md.tmpl
  • internal/templater/templates/spec/tasks-unit.md.tmpl
  • internal/templater/templates/spec/tasks.md.tmpl
  • internal/validator/validator.go
  • internal/validator/validator_test.go

Comment thread internal/cli/spec.go
Comment on lines +492 to +494
if opts.Artifact == "tasks" && effectiveFlow(data.DevelopmentFlow) == "unit" {
pair[1] = "templates/spec/tasks-unit.md.tmpl"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize the flow before selecting the tasks template.

validator.specDevelopmentFlow trims development_flow, so a value such as " " resolves to unit. SpecGenerate calls effectiveFlow without trimming, falls through to the TDD template, and produces RED/GREEN tasks that validation rejects. Normalize in effectiveFlow and add a whitespace-only regression test.

Proposed fix
 func effectiveFlow(f string) string {
+	f = strings.TrimSpace(f)
 	if f == "" {
 		return defaultDevelopmentFlow
 	}
 	return f
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/spec.go` around lines 492 - 494, Update effectiveFlow to trim
surrounding whitespace from development_flow before determining the effective
flow, keeping template selection in SpecGenerate consistent with
validator.specDevelopmentFlow. Add a regression test covering a whitespace-only
development_flow and verify it selects the unit tasks template rather than the
TDD template.

Comment thread internal/plan/brief.go
Comment on lines +111 to 128
// 3b. Governing decisions — refs only. The brief used to inline each cited
// ADR's title + body so a design could not silently ignore the decisions it
// was bound to; with the bodies gone, `designConformance` requires the
// authored design.md to cite each governing adr:<slug>, and the session
// fetches the decision itself via `csdd graph explain adr:<slug>`.
if len(feat.ADRRefs) > 0 {
adrs := ScanADRs(root)
w("## Decisions (docs/adr — the why)\n\n")
w("## Governing decisions (docs/adr — fetch the why; cite each in design)\n\n")
w("The brief no longer inlines ADR bodies. For each governor run `csdd graph explain adr:<slug>` for the decision; `csdd spec validate` requires your design.md to cite it.\n\n")
for _, slug := range feat.ADRRefs {
adr, res := adrs.Resolve(slug)
if res != ADRResolved {
w("- **adr:%s** — WARNING: does not resolve to a docs/adr record (validate should have caught this).\n", slug)
if _, res := adrs.Resolve(slug); res != ADRResolved {
w("- adr:%s — WARNING: does not resolve to a docs/adr record (validate should have caught this).\n", slug)
continue
}
w("### %s (adr:%s)\n\n", orDash(adr.Title), adr.Slug)
if adr.Status == ADRStatusSuperseded {
w("_status: superseded")
if adr.SupersededBy != 0 {
w(" by %s", fourDigit(adr.SupersededBy))
}
w("_\n\n")
}
if adr.Body != "" {
w("%s\n\n", adr.Body)
}
w("- adr:%s\n", slug)
}
w("\n")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: confirm which CLI command invokes ValidatePlan/designConformance,
# and whether spec validate / the autonomous loop ever calls plan validate.
rg -n "ValidatePlan\(" internal/cli/plan.go internal/cli/spec.go
rg -n -B3 -A3 '"validate"' internal/cli/plan.go internal/cli/spec.go
rg -n "designConformance|plan.*validate" internal/plan/runner.go internal/plan/verify.go 2>/dev/null

Repository: protonspy/csdd

Length of output: 956


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant command wiring and the autonomous loop.
sed -n '1,240p' internal/cli/plan.go
printf '\n--- SPEC ---\n'
sed -n '1,240p' internal/cli/spec.go
printf '\n--- PLAN VALIDATION ---\n'
sed -n '1,260p' internal/plan/validate.go
printf '\n--- BRIEF ---\n'
sed -n '80,150p' internal/plan/brief.go
printf '\n--- LOOP SEARCH ---\n'
rg -n "spec validate|plan validate|ValidatePlan|designConformance|quality-gate|done|self-check|acceptance" internal/plan internal/cli -g '!**/*_test.go'

Repository: protonspy/csdd

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact validation implementations and the brief's self-check / acceptance gates.
sed -n '280,390p' internal/cli/plan.go
printf '\n--- SPEC VALIDATE ---\n'
sed -n '720,840p' internal/cli/spec.go
printf '\n--- BRIEF SELF-CHECKS ---\n'
sed -n '160,205p' internal/plan/brief.go
printf '\n--- DESIGN CONFORMANCE USAGE ---\n'
rg -n "designConformance\(" internal/plan internal/cli

Repository: protonspy/csdd

Length of output: 9150


Point this brief at csdd plan validate, not csdd spec validate.
designConformance is enforced by plan validate, so this text points readers to the wrong check; if ADR citations must block done, the self-checks should mention plan validate too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/plan/brief.go` around lines 111 - 128, Update the
governing-decisions text in the brief generation block to reference “csdd plan
validate” instead of “csdd spec validate,” including the self-check guidance for
ADR citations. Keep the existing ADR resolution and design citation requirements
unchanged.

Comment thread internal/plan/cost.go Outdated
Comment thread internal/templater/templates/spec/tasks-unit.md.tmpl
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant