← Back to list

Token economy with SDD: How to burn GPUs the right way?

NOTE: All code examples in the appendix sections are experiments. I am sure there are better ways to do this. Take ideas and write better.

Atiur Siddique · 2026-06-07 01:42 · 0 claps · 36.4 min read
#ai-token-economy #spec-driven-development #sdd
Open on Medium ↗
Wiki topics: OPS · LLMOps & Inference 🔬 · Science · General

Token economy with SDD: How to burn GPUs the right way?

NOTE: All code examples in the appendix sections are experiments. I am sure there are better ways to do this. Take ideas and write better.

Company Blew $500M On Claude AI In One Month — Yahoo Finance

We, software engineers, used to turn coffee and pizza into code, now we need AI tokens also (I hear uncle Roger saying “Haiyaa..” in the background). When tokens are used right, they can work like NOS on your car — a sudden boost in performance and speed that may get us a win. However, overuse, underuse, and timing all have consequences. This post is about using AI tokens the right way, specifically, in the context of SDD (spec driven development).

When we start a new software development project, we are heavily invested into the pillars of well architected framework — security, scalability, performance guarantee, and cost optimization; but a lot less into development economy. With the rise of SDD, does that need to change though? Let’s think about what changes with SDD adoption from our previous AI powered codegen days.

This writeup brings awareness and clarity to 6 ways to cut our SDD token bill:

  1. Validate specs before coding (catch errors early, not expensively)
  2. Feed interface skeletons, not full implementations
  3. Pack only task-relevant context (not the whole repo)
  4. Retrieve only relevant spec sections (context-aware RAG)
  5. Use prompt caching to avoid re-reading static content
  6. Cascade models — use cheap models for simple tasks

Let’s get to it.

What are different or new in SDD?

With SDD, a general first learning is that SDD tools provide the right kind of context in our Generative AI calls for code generation. It keeps the original intent, guidelines (read “constitution”), PRD (product requirement document), and task breakdown intact which gets fed into the right code generation task’s context. Such context management makes sure we do not drift away into a vibe coding stream (no offence “lovable”, you are loved). We also do not suffer from an incomplete, incoherent, half-ambiguous context as we frequented with traditional codegen tools. That is because good SDD tools have some mechanism, like Kiro’s 3 stage neuro symbolic pipeline or the traditional “interview me first” prompts, to keep that context almost complete and ambiguity free. This is why, with SDD, we start to see almost acceptable code and we feel excited. That is right before we go ahead and add a ton of spec (specification) or constitution, security guidance, disaster recovery and fault tolerance guidance, or make up skills that are rarely used. And, kids, that’s why we can’t have nice things around here. This last mistake on bloating up guidance can be a story for another day, bureaucracy existed before SDD.

The second learning is now we have too much prompting and too much to review. We almost alleviated the first bottleneck in software development, i.e., coding. Now, who is gonna review all this crap, a.k.a. the generated code? An obvious thought is to have another agentic solution review and bar raise the generated code. Is that approach safe enough though? Remember Carlos’ post on can we have the same LLM (think “similar” also) write code and get them review also (by the same or similar LLM)? We are genuinely worried. How do we maintain our code and devops bar following well architected framework now? Also, do I have to write prompts all the time? The code is not getting exactly right even after multiple attempts (my colleague asked me this yesterday). Should I just go ahead and write the code myself? Will that leave my spec, PRD, and code inconsistent and result in a thrashing later on? Another related problem — if I make a small change in my PRD or constitution, why do I sometimes get a few thousand line of code change? These problems are also studied to some extent and can be fun topic for later.

A close third learning is my code gets worse as I add more and more guardrails and my teams start to reuse company level AI guidances everywhere. Then we start to drop in relevance and accuracy of generated code suddenly and a surge in token cost. Yes, here the “200K token trap”, the “token cliff”, the “lost in the middle”, and all that jazz comes into play. Understanding how the attention heads inside a transformer model works and along with FFN to make sense of these. Again, leaving this for another day to get to today’s topic.

The fourth learning occurs when the invoice arrives. Or, in my case, when the LLM quota is up. What do they mean that I finished my whole month’s quota? I only built maybe 5 projects this week and they are not even that big. Let’s discuss this fourth one today. In all fairness, the first 3 are fun topics also and I have recently held a brownbag on those. I am just organizing my thoughts on this fourth one today. Specially because Michael (my colleague) asked me about this and I owe him a good answer.

The cost struggle with AI tokens

Related to this 4th issue, recently, multiple enterprise leadership asked a grounding question: Why is our AI bill so massive? Ranging from Google CEO Sundar Pichai, Uber COO Andrew Macdonald, Box CEO Aaron Levie to OpenAI CEO Sam Altman all have recently expressed concern with AI cost. Let’s look at this cost, and again, in the context of SDD.

When we use SDD, the token cost figures escapes us first, for a good reason. If a team writes 10,000 lines of new code in a week, the bill may show we processed tens of millions of tokens. The extra cost isn’t coming from what the model is writing. It’s coming from what the model is reading, most of the time anyway. The input context suddenly became massive with SDD oriented development and started to dictate our token expense, and, that also, for almost a good reason. To make sure the transformer models produce correct code for a task from our task breakdown, most IDEs today uses Agentic workflows that sends the complete spec, constitution, and PRD for that one tiny task which sometimes can be as simple as “write unit test for this function that sorts the queued item by timestamp”. The whole spec, constitution, and PRD are not needed in that task but someone smart has to tell the agents that. Thankfully, some of the IDEs have started to become smarter about this but most still have not received that memo. As a result, in a standard SDD setup, input tokens routinely make up 80% to 90% of total spend. A lot of naive configurations effectively force the model to re-read our entire architectural blueprint every single time we ask it to change a door handle. We are paying for a full structural survey of our home just to hang a picture frame on the back wall. Kind of. Let’s look at some data to understand the magnitude of this problem.

Let’s do a rough estimate of the cost of implementing an individual task in a standard project. A typical context dump may carry 2,000 tokens of system instructions, 8,000 tokens of architecture docs, 3,000 tokens of API contracts, and 5,000 tokens of adjacent code. That is 23,000 tokens going in to get 1,500 tokens of code coming out — a 15:1 ratio. The more complete our engineering specs get, the more we penalize ourselves on cost. In most codegen flows it repeats for all tasks. It seems weird and stupid. If an LLM were a person, it would be like repeating the entirety of company’s rules, design principles, well architected framework pillars, product docs, UX/UI design, sprint task breakdown to date, only to assign a small task of fix this bug — we would be frustrated.

How do we fix this? The fix cannot be grossly reducing or limiting spec. Thin or reduced specs can easily be incomplete and we are back into LLM hallucinations or incoherent, incomplete code. Instead, we need to maintain context hygiene around LLM calls. Teams and open source projects have been looking into various tooling for such ideas, separately. This blog is an agent skill based approach to find an easy way of doing the same and implement these ideas together. VScode type IDEs preloads skill directories with SKILL.md files. These skills remain available for us to call directly or for the Agents to call (via LLM instructions). Using skills give us 2 benefits: 1/ human readable tool instructions contain most of the thought process which is easy to iterate through and 2/ they are only included in LLM context when the corresponding skill is invoked. Otherwise, we are paying for the tokens of skill definitions only which are small.

Below are some cost reduction ideas and example skills. While the examples target VScode, because VScode is open source, these should apply to all AI backed IDEs and some of them may have these bultin already.

Make sure the spec is clear, correct, complete, and coherent

Before SDD and vibe coding, we would review PRFAQ and PRD (product requirement document) docs formally, even before technical design review. Because a wrong assumption or an infeasible requirement will burn the dev team trying build something that was not the intention. But the process from ideation, inception, PRFAQ, funding, PRD, design review (or TDR technical design review) would be spanned across weeks or months. This gave us plenty of time to wonder and flag any bad ideas. With SDD, the inception to implementation is happening within hours — leaving us no time to flag these naturally. We must detect gaps, ambiguity, infeasibility, incoherence in minutes to hours. This means we have to use agent skills inside the flow of SDD. Ideally, we should execute this right after “spec” step in spec-kit or “requirements” step in Kiro. An example agent skill for VScode is written in section “Appendix — Agent skill review-spec”. This example, currently, looks for “spec.md” as in VScode. Change that to “.kiro/requirements.md” if you are using Kiro.

This idea is not exactly new. We do ask LLMs in vibe coding by saying “Interview me first to clarify details and don’t assume anything. Double check for any assumptions and clarify with me before implementation.” In agentic workflows in service or applications, we use LLM as a judge, where we generally implement a mechanism to challenge and interrogate the intention and the ask. Kiro has a 3 stage neuro symbolic pipeline which I assume does something similar. There is a blog referring to catching spec mistakes early in favor fail-fast approach. Same goal we have here. However, our version does not require the spec to be written in EARS format the way Kiro writes spec. We allowed natural language based spec that is compatible with VScode plugin spec-kit. This is both good and bad. EARS is a mature standard on writing clear specs — something very clearly understandable by machines. It IS human readable, but someone who is not familiar with EARS standard, can find it confusing and confusions lead to bad decisions. So, we allowed natural language here and we can support EARS also (it should by default as EARS is also English) but we did not force EARS validation or requiring EARS as the only supported spec format.

Generate Interface Skeletons

An AI agent rarely needs to read a 400-line implementation of an authentication service to understand how to call it. It just needs the contract — the API, SDK. Similarly, AI should not need hundreds of lines of code of a class (btw, try not to create classes that big) to understand how to use it. It should only need javadoc/docstrings of the class, the functions, types, etc. which the declaration or the skeleton.

We can build an agent skill that strips the body/definition of functions and classes keeping only the declaration before they enter the prompt context. There are existing tools to do the heavy lifting, like ts-morph. We just need an agent skill that uses it this way. By feeding only the skeletons into the AI context, a 20,000-token file dump can instantly reduce to a 2,000-token summary. It cuts input costs by more than half without losing an ounce of type safety.

An example agent skill for generating the skeleton file is written in section “Appendix — Agent skill generate-skeleton”. Note that we probably need to instruct our LLM invokers to only use the skeleton that needs a bit of extra work. I did not cover that since the tools will be different for every one based on their choice of VScode plugins. Currently, it generates an skeleton file “all-declarations.md”. We should invoke the skill only when we common classes are updated.

Task-Scoped Context Packing

Repository packing with tools like repomix can curate a codebase into a much smaller and relevant context file. Most teams do not use aggressive repomix’s “ignore patterns” in FOMO but that is what could help us here. Specially, if we pack in task-scoped way where API code generation vs database schema generation utilizes different context that are appropriate for that task. Instead of an “everything, always” strategy, our task-scoped code context strategy feeds the right OAS API spec or SDK spec where we need to call an external API and a database schema with connection config when we are accessing a NOSQL or RDBMS. Our agent skill can detect our intent, figure out what part of existing code and config are required and meaningful and only feeds that bit. An example is in “Appendix — Agent skill context-packing” but this skill also has to be hooked into codegen calls. In our common “constitution.md” file we have to put some as required that says always use the agent skill “context-packing” to find which files should be fed into a code generation task instead of sending all files.

Context Aware Spec Retrieval

Every single coding task does not require our entire corporate engineering manual. We don’t have to to teach the complete threat model and security posture guidance to a backend SDE who is writing a piece of code to fetch metadata from our control plane. Ideally they should learn those, but for growth. For this task, they only need to know how not relax our security boundary in that task. I am not sure how or whether SDD tools are addressing this honestly, since it is a little questionable and may go against why we are doing SDD in the first place. But it is an idea I see is inevitable in the future due to correctness if not cost.

Here, before passing our spec folder to the model, a pre-pass step reads our task requirements, matches them against our markdown folder layout, and picks only the paragraphs that directly impact the feature at hand. This can be either relevance based as any RAG does or we can leverage a low-end (read “cheap”) LLM to decide how to extract relevant chunks from large docs. The targeted context produces sharper, more architecturally consistent code than a massive, undifferentiated data dump. While this skill tries to solve a cost optimization issue, it also solves a correctness challenge.

I have intentionally skipped writing this as a separate tool as some of the IDEs have a built-in capability to use RAG at file level while others use plugins. None to my knowledge uses chunked version of relevant sections only. A second reason is to acknowledge my experimentations have not finished on this. It is a questionable approach so it is ok to defer this for now.

LLM-Provider Cache Aware Prompt

Prompt caching is arguably the easiest way to drop cloud costs, yet it is frequently missed. Major LLM providers allow us to store static text — which can include our core framework rules, coding standards, and security guardrails — on their (provider’s) side after the first call. Subsequent requests read from that cache at roughly a tenth of the price. To use it properly, we have pay attention to provider’s documents. Generally speaking, to make this work, our static content must come first in your system prompt payload, followed by the dynamic, task-specific details at the very end. This is because some providers match prefix to determine whether they can use cached tokens. To solve this, we can structure our system instructions to strictly enforce this hierarchy. Our architectural skeletons and base rules stay pinned at the top of the context window. As long as the team doesn’t alter those top lines during a coding session, the provider hits the cache automatically. We stop paying full price for text the model read five seconds ago.

Model cascading

Not every step in SDD workflow requires a premium LLM model with reasoning support. We know Opus generates way better code than Sonnet. But do I need Opus to decide the next tool call from a list of 3 functions? There are reasons we have the lite models in gemini, Claude, and GPT series. We can use lightweight models for simple, deterministic tasks and use top-tier models for high-ambiguity tasks. Low/medium quality models can be used for code generation also where ambiguity is low for example, unit code generation or CDK/CFN/infrastructure code generation. Thankfully, most IDEs are doing this already in their agent mode, VScode and Kiro included. But we still see 2 mistakes: 1/ engineers often override the configurations to always stay on the top LLMs in FOMO and 2/ we forget to attach a specific model in custom skills. As a result, the cost-benefit ratio drops.

This strategy does not need a separate skill but us understanding that we can pass our full context to a top-tier reasoning model with an explicit prompt directing it to output only a highly precise, structured implementation plan written in descriptive pseudocode. Because it contains no boilerplate or production syntax, it uses very few tokens. Second, we hand that plan and our target interface skeleton to a faster, significantly cheaper worker model. In case of complex code, we can use high-end models here also leaving test code generation, UI code generation, and infra code generation to low-end models. These task implementation calls doesn’t need to see the entire architecture document because the plan already extracted the necessary logic. Therefore, the ambiguity reduces, the attention heads in our transformer extracts the right relationships across tokens, and we still get superb result. This approach, therefore, still attains almost same accuracy but gets the work done in an order of magnitude cheaper token cost.

Peril of oversights — token limit, long context, and priority mode

Most AI providers charge extra if we cross the token limits. The extra cost is not geometric. Per token cost can be 2 to 6 times of standard cost. Similarly, priority mode in GPT and fast mode in Claude charges higher rate. Due to a easy-to-make oversight, we may end up paying 2 to 3 times than we should have. For example, let’s say we use gpt-5.5 to generate 1M tokens (for easier math). Using current gpt-5.5 pricing as of Jun 6, we find below. Output cost: $30 Input cost: $75 (using the previous 15 to 1 input-output ratio example) Total cost: $105

If I switched to priority mode for something temporarily and forgot to switch back to standard, the same cost becomes $75+15x$12.5 = $262.5 . That is $157.5 loss due to a simple oversight. Same thing can happen from fast mode or long context.

Some providers offer rate limiting than charging for long context or priority modes. Claude seem to have removed pricing on long context recently. Meta point is, check your provider’s pricing page and pay attention to fine prints.

Conclusion

The token bill is ultimately a signal of devops maturity. More than just money, it hints at bloated guidance, low-relevance code generation, and low bar on attention to details. We need to pay attention to what goes into the transformer and what comes out from multiple angles instead of trusting the IDEs will all the right things for us.

Finally, the example skills in this write up are examples to explain the ideas while I am trying to understand what works. They are not meant to be prescriptive solutions for anyone.

Appendix — Agent skill review-spec

review-spec/SKILL.md

---
name: review-spec
description: Review spec.md product requirement and specification files from spec-driven development for clarity, completeness, and coherence before design or implementation. Use when asked to inspect, critique, validate, improve, or update a VS Code spec-it/spec.md file, identify gaps, ambiguity, contradictions, missing acceptance criteria, unresolved assumptions, or revise the spec from user feedback.
argument-hint: "[spec.md path or review/update request]"
user-invocable: true
---

# Review Spec

## Overview

Review a `spec.md` file before the design step so later agents do not need to invent requirements. Produce a precise list of gaps, ambiguities, and incoherence, then update `spec.md` only from user-approved feedback.

Use the checklist in [Spec Review Rubric](references/spec-review-rubric.md). Run [scripts/spec_lint.py](scripts/spec_lint.py) for a mechanical scan when working with a local file.

## File Selection

1. If the user provides a path, use that file.
2. Otherwise, look for `spec.md` in the current workspace.
3. If one `spec.md` exists, use it.
4. If multiple `spec.md` files exist, list them and ask which one to review.
5. If no `spec.md` exists, ask for the path or for the spec content.

Do not review from memory. Read the actual file contents before judging the spec.

## Review Workflow

1. Read the whole `spec.md`. For long files, first map headings, then inspect each section.
2. Run the mechanical scan when possible:

```bash
python3 scripts/spec_lint.py /path/to/spec.md
  1. Review for completeness:
    • Problem, goal, users, scope, non-goals, definitions, assumptions, dependencies, constraints.
    • Functional requirements, business rules, user journeys, edge cases, error states, permissions.
    • Data requirements, integrations, APIs, migration/backward compatibility, performance, reliability.
    • Security, privacy, compliance, accessibility, localization, observability, analytics, rollout.
    • Acceptance criteria, testability, validation data, success metrics, and release gates.
  2. Review for clarity:
    • Each requirement has actor, action, object, condition, and expected outcome where applicable.
    • Terms are defined and used consistently.
    • Vague words such as "fast", "easy", "robust", "soon", or "seamless" are quantified or replaced.
    • Priorities and must/should/could language are explicit.
  3. Review for coherence:
    • Requirements do not contradict each other.
    • Constraints align with scope, goals, personas, and acceptance criteria.
    • Duplicates are either merged or intentionally differentiated.
    • Out-of-scope statements do not conflict with required behavior.
  4. Separate evidence from inference. Cite file sections or line numbers for each finding when possible.
  5. Do not fill missing product decisions with assumptions. Phrase them as questions or proposed amendments.

Report Format

Return this structure:

## Spec Review: spec.md

Readiness: Ready for design | Needs clarification | Blocked

Summary:
- ...

Findings:
| ID | Type | Severity | Location | Evidence | Impact | Question or Proposed Fix |
| --- | --- | --- | --- | --- | --- | --- |
| RS-001 | Gap | Blocker | ... | ... | ... | ... |

Design-Blocking Unknowns:
- ...

Suggested Spec Amendments:
- ...

Next Step:
- ...

Use severity this way:

  • Blocker: design would require inventing behavior or resolving a contradiction.
  • Major: implementation can begin only with meaningful risk or rework.
  • Minor: wording, organization, or traceability issue that should be cleaned up.

Update Workflow

Only update spec.md after one of these is true:

  • The user explicitly asks to update the file.
  • The user answers review questions and asks to apply the answers.
  • The user approves a proposed amendment list.

When updating:

  • Preserve the existing structure and intent of the spec.
  • Make the smallest edits that remove the confirmed gap, ambiguity, or contradiction.
  • Use the user's words and decisions as the source of truth.
  • Do not silently invent acceptance criteria, metrics, integrations, dates, roles, or constraints.
  • If the user wants placeholders retained, mark them as TODO(decision-needed): ....
  • After editing, re-read the changed sections and report what changed.

Examples

/review-spec

Review the workspace spec.md and report gaps, ambiguities, and contradictions.

/review-spec specs/checkout/spec.md

Review a specific spec file.

/review-spec update spec.md using these answers: retries are limited to 3, timeout is 10 seconds, admins can override failed payments.

Update spec.md from user-provided decisions, then summarize the edits.


review-spec/scripts/spec_lint.py

!/usr/bin/env python3

"""Mechanical lint scan for spec.md files."""

from future import annotations

import argparse import json import re import sys from collections import Counter from pathlib import Path

VAGUE_TERMS = { "fast", "quick", "quickly", "easy", "easily", "simple", "robust", "seamless", "soon", "appropriate", "optimized", "better", "improved", "minimal", "maximum", "large", "small", "user-friendly", } WEAK_MODALS = {"should", "could", "may", "might", "ideally", "as needed"} PLACEHOLDERRE = re.compile(r"(<[^>\n]+>|[[A-Z0-9 -]{3,}]|\bTBD\b|\bTODO\b|\bFIXME\b)", re.IGNORECASE) HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*$") REQUIREMENT_RE = re.compile(r"\b(must|shall|required|requires?|should|may|can|will)\b", re.IGNORECASE)

def find_spec(start: Path) -> Path | None: if start.is_file(): return start direct = start / "spec.md" if direct.exists(): return direct matches = sorted(start.rglob("spec.md")) if len(matches) == 1: return matches[0] return None

def line_collections(text: str) -> list[tuple[int, str]]: return [(idx, line.rstrip("\n")) for idx, line in enumerate(text.splitlines(), start=1)]

def scan(path: Path) -> dict: text = path.read_text(encoding="utf-8") lines = line_collections(text) headings: list[dict] = [] placeholders: list[dict] = [] vague: list[dict] = [] weak_modals: list[dict] = [] requirements: list[dict] = []

for line_no, line in lines:
    heading_match = HEADING_RE.match(line)
    if heading_match:
        headings.append(
            {
                "line": line_no,
                "level": len(heading_match.group(1)),
                "title": heading_match.group(2).strip(),
            }
        )

    for match in PLACEHOLDER_RE.finditer(line):
        placeholders.append({"line": line_no, "text": match.group(0), "context": line.strip()})

    words = re.findall(r"[A-Za-z][A-Za-z-]*", line.lower())
    vague_hits = sorted(set(words) & VAGUE_TERMS)
    if vague_hits:
        vague.append({"line": line_no, "terms": vague_hits, "context": line.strip()})

    weak_hits = sorted(set(words) & WEAK_MODALS)
    if weak_hits:
        weak_modals.append({"line": line_no, "terms": weak_hits, "context": line.strip()})

    if REQUIREMENT_RE.search(line):
        requirements.append({"line": line_no, "text": line.strip()})

title_counts = Counter(heading["title"].lower() for heading in headings)
duplicate_headings = [
    {"title": title, "count": count}
    for title, count in sorted(title_counts.items())
    if count > 1
]

expected_sections = {
    "goals": ["goal", "objective", "success"],
    "scope": ["scope", "non-goal", "out of scope"],
    "users": ["user", "persona", "actor", "role"],
    "requirements": ["requirement", "functional"],
    "acceptance": ["acceptance", "criteria", "test"],
    "edge_cases": ["edge", "error", "failure", "exception"],
    "constraints": ["constraint", "assumption", "dependency"],
}
heading_blob = " ".join(heading["title"].lower() for heading in headings)
missing_section_hints = [
    name
    for name, aliases in expected_sections.items()
    if not any(alias in heading_blob for alias in aliases)
]

return {
    "path": str(path),
    "line_count": len(lines),
    "heading_count": len(headings),
    "requirement_like_line_count": len(requirements),
    "headings": headings,
    "duplicate_headings": duplicate_headings,
    "placeholders": placeholders,
    "vague_terms": vague,
    "weak_modals": weak_modals,
    "missing_section_hints": missing_section_hints,
}

def print_markdown(result: dict) -> None: print(f"# Mechanical Spec Scan: {result['path']}") print() print(f"- Lines: {result['line_count']}") print(f"- Headings: {result['heading_count']}") print(f"- Requirement-like lines: {result['requirement_like_line_count']}") print()

if result["missing_section_hints"]:
    print("## Missing Section Hints")
    for item in result["missing_section_hints"]:
        print(f"- {item}")
    print()

for title, key in (
    ("Duplicate Headings", "duplicate_headings"),
    ("Placeholders", "placeholders"),
    ("Vague Terms", "vague_terms"),
    ("Weak Modals", "weak_modals"),
):
    print(f"## {title}")
    entries = result[key]
    if not entries:
        print("- None found")
        print()
        continue
    for entry in entries[:50]:
        line = entry.get("line")
        prefix = f"line {line}: " if line else ""
        if "context" in entry:
            print(f"- {prefix}{entry.get('context')}")
        else:
            print(f"- {entry}")
    if len(entries) > 50:
        print(f"- ... {len(entries) - 50} more")
    print()

def main() -> int: parser = argparse.ArgumentParser(description=doc) parser.add_argument("path", nargs="?", default=".", help="spec.md file or workspace directory") parser.add_argument("--json", action="store_true", help="emit JSON") args = parser.parse_args()

input_path = Path(args.path).resolve()
spec_path = find_spec(input_path)
if spec_path is None:
    print(f"ERROR: could not find exactly one spec.md from {input_path}", file=sys.stderr)
    return 2
if not spec_path.exists():
    print(f"ERROR: file does not exist: {spec_path}", file=sys.stderr)
    return 2

result = scan(spec_path)
if args.json:
    print(json.dumps(result, indent=2, sort_keys=True))
else:
    print_markdown(result)
return 0

if name == "main": sys.exit(main())


review-spec/references/spec-review-rubric.md

Spec Review Rubric

Use this rubric to review product requirements or specifications before design.

Completeness

Check whether the spec has enough information for a design agent to proceed without assumptions:

  • Problem statement and intended outcome.
  • Target users, roles, permissions, and user goals.
  • In-scope and out-of-scope behavior.
  • Terms, domain concepts, and acronyms.
  • Functional requirements with actor, action, object, condition, and result.
  • User flows, alternate flows, edge cases, empty states, error states, retries, and recovery behavior.
  • Business rules, prioritization, constraints, dependencies, and assumptions.
  • Data inputs, outputs, lifecycle, retention, migration, and deletion behavior.
  • External systems, APIs, events, integrations, rate limits, and failure handling.
  • Nonfunctional requirements: performance, scalability, availability, reliability, observability, supportability.
  • Security, privacy, compliance, auditability, accessibility, localization, and abuse prevention.
  • Acceptance criteria, measurable success metrics, testing expectations, and release or rollout gates.

Clarity

Flag wording that could lead to multiple interpretations:

  • Vague adjectives or adverbs: fast, simple, easy, robust, seamless, soon, appropriate, optimized.
  • Undefined comparative terms: better, improved, minimal, maximum, large, small.
  • Missing thresholds, units, time windows, limits, or priority.
  • Ambiguous actors: user, admin, system, service, customer, owner.
  • Passive voice that hides responsibility.
  • Requirements that combine multiple behaviors without clear ordering.
  • Optionality without decision rules: may, might, should, ideally, as needed.

Good requirements usually state:

When [condition], [actor/system] must [action] [object] so that [outcome], with [limit/constraint].

Coherence

Look for contradictions and internal drift:

  • A requirement conflicts with a non-goal or out-of-scope statement.
  • A user role is allowed and denied the same action.
  • Acceptance criteria verify behavior not required elsewhere.
  • Constraints make a required workflow impossible.
  • Two sections use different names for the same concept.
  • Two similar requirements have different limits without explaining why.
  • Priority labels conflict with release gates or must-have language.
  • Error handling contradicts data retention, audit, privacy, or security requirements.

Finding Types

  • Gap: Required information is missing.
  • Ambiguity: Wording supports multiple plausible interpretations.
  • Incoherence: Requirements contradict or undermine each other.
  • Unverifiable: A requirement cannot be tested as written.
  • Traceability: A goal, requirement, or acceptance criterion is disconnected from the rest of the spec.

Severity

  • Blocker: A design or implementation would require inventing product behavior.
  • Major: Work could proceed, but risk of wrong design or rework is high.
  • Minor: Quality issue that should be fixed but does not block design.

Update Principles

  • Resolve findings with user-provided decisions.
  • Preserve the spec's existing organization unless structure itself causes confusion.
  • Prefer precise language over broad rewriting.
  • Keep unanswered decisions visible instead of burying them as assumptions.
  • Re-check changed text for new contradictions.

Appendix — Agent skill generate-skeleton

generate-skeleton/SKILL.md

---
name: write-declarations
description: Generate all-declarations.md API skeleton files for VS Code workspaces by extracting class, function, method, interface, type, enum, docstring, JSDoc/Javadoc-style comment, parameter, and return/type declarations while omitting implementation bodies. Use when asked to reduce source files into declaration-only context for code generation LLM calls, SDK/API understanding, prompt compression, or contract review.
argument-hint: "[workspace path or source files]"
user-invocable: true
---

# Write Declarations

## Overview

Create `all-declarations.md`, a compact contract file that preserves how to call code without dumping implementation bodies into the prompt. Prefer deterministic extraction over summarization so function signatures, class contracts, comments, parameters, and types remain faithful to the source.

Use [Declaration Extraction Rules](references/declaration-extraction-rules.md) for scope and quality criteria. Use [scripts/write_all_declarations.py](scripts/write_all_declarations.py) to generate the file, which delegates TypeScript/JavaScript parsing to [scripts/extract_ts_declarations.mjs](scripts/extract_ts_declarations.mjs) when `ts-morph` is available.

## Default Command

From the target workspace root:

```bash
python3 /path/to/write-declarations/scripts/write_all_declarations.py . --output all-declarations.md

If invoked from inside this skill folder, pass the user's project path explicitly.

Workflow

  1. Identify the target workspace or files. Default to the current workspace.
  2. Confirm the output path. Default to all-declarations.md in the target workspace root.
  3. Avoid generated and dependency directories such as .git, node_modules, dist, build, coverage, .venv, and __pycache__.
  4. Run the bundled extractor.
  5. If TypeScript/JavaScript files exist and ts-morph is not available, tell the user:
    • Python and Java best-effort extraction still ran.
    • TypeScript/JavaScript needs ts-morph and typescript installed in the project or available to Node.
    • Ask before installing packages or using network access.
  6. Inspect the generated all-declarations.md for obvious failures:
    • It should contain file sections, declarations, comments/docstrings, and no large function bodies.
    • It should not include node_modules, build output, lockfiles, minified bundles, secrets, or binary data.
    • It should identify skipped or unsupported languages.
  7. Report the output path, approximate source file count, and any extraction warnings.

Output Contract

all-declarations.md should include:

  • A generation header with source root and timestamp.
  • One section per language and file.
  • Class declarations without implementation bodies.
  • Constructor, method, function, and arrow-function signatures.
  • Interfaces, type aliases, enums, and exported constants when available.
  • JSDoc/Javadoc-style comments, Python docstrings, parameters, return types, decorators, inheritance, and visibility/modifier keywords.
  • Explicit warnings for files skipped due to parse errors or missing parser dependencies.

Do not hand-summarize a large file as a substitute for extraction unless parsing is impossible. When fallback summarization is needed, label it clearly as Manual fallback.

Language Support

  • TypeScript/JavaScript: use ts-morph through extract_ts_declarations.mjs.
  • Python: use Python ast through write_all_declarations.py.
  • Java: use the bundled best-effort scanner for public/protected/private class and method signatures with nearby Javadoc comments.
  • Other languages: state that the extractor does not support them yet and suggest adding a parser-specific script.

Update Behavior

If all-declarations.md already exists, overwrite it only when the user asked to regenerate declarations. Preserve no manual edits inside that file unless the user explicitly asks for a merge.

After generation, suggest feeding all-declarations.md to code generation or design calls instead of raw implementation files when the task only needs API contracts.

Examples

/write-declarations

Generate all-declarations.md for the current workspace.

/write-declarations packages/auth --output docs/all-declarations.md

Generate declarations for a package and write the contract file under docs/.

/write-declarations src/auth.ts src/session.py

Generate declarations from specific source files.


generate-skeleton/scripts/write_all_declarations.py

!/usr/bin/env python3

"""Generate all-declarations.md from source files."""

from future import annotations

import argparse import ast import datetime as dt import fnmatch import os import subprocess import sys from dataclasses import dataclass from pathlib import Path

DEFAULT_EXCLUDE_DIRS = { ".git", ".hg", ".svn", ".idea", ".vscode", ".venv", "venv", "pycache", "node_modules", "dist", "build", "coverage", ".next", ".nuxt", "target", "out", } TS_EXTENSIONS = {".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"} PY_EXTENSIONS = {".py"} JAVA_EXTENSIONS = {".java"}

@dataclass class ExtractionResult: title: str markdown: str warnings: list[str] file_count: int = 0

def rel(path: Path, root: Path) -> str: try: return str(path.relative_to(root)) except ValueError: return str(path)

def should_skip(path: Path, root: Path, excludes: list[str]) -> bool: parts = set(path.relative_to(root).parts if path.is_relative_to(root) else path.parts) if parts & DEFAULT_EXCLUDE_DIRS: return True text = rel(path, root) return any(fnmatch.fnmatch(text, pattern) for pattern in excludes)

def collect_files(inputs: list[Path], excludes: list[str]) -> tuple[Path, list[Path]]: existing = [path.resolve() for path in inputs if path.exists()] if not existing: raise FileNotFoundError("No input paths exist")

root = existing[0] if existing[0].is_dir() else existing[0].parent
if len(existing) > 1:
    common = os.path.commonpath([str(path if path.is_dir() else path.parent) for path in existing])
    root = Path(common).resolve()

files: list[Path] = []
for item in existing:
    if item.is_file():
        if not should_skip(item, root, excludes):
            files.append(item)
        continue
    for path in item.rglob("*"):
        if path.is_file() and not should_skip(path, root, excludes):
            files.append(path)

source_files = [
    path
    for path in sorted(set(files))
    if path.suffix.lower() in TS_EXTENSIONS | PY_EXTENSIONS | JAVA_EXTENSIONS
    and not path.name.endswith(".min.js")
]
return root, source_files

def unparse(node: ast.AST | None) -> str: if node is None: return "" try: return ast.unparse(node) except Exception: # noqa: BLE001 return "..."

def defaults_for(args: list[ast.arg], defaults: list[ast.expr]) -> dict[str, str]: result: dict[str, str] = {} if not defaults: return result padded: list[ast.expr | None] = [None] * (len(args) - len(defaults)) + list(defaults) for arg, default in zip(args, padded): if default is not None: result[arg.arg] = "..." return result

def format_arg(arg: ast.arg, default: str | None = None) -> str: text = arg.arg if arg.annotation is not None: text += f": {unparse(arg.annotation)}" if default is not None: text += f" = {default}" return text

def format_signature(node: ast.FunctionDef | ast.AsyncFunctionDef, , one_line: bool = False) -> str: args = node.args positional = list(args.posonlyargs) + list(args.args) positional_defaults = defaults_for(positional, list(args.defaults)) parts = [format_arg(arg, positional_defaults.get(arg.arg)) for arg in positional] if args.vararg: parts.append("" + format_arg(args.vararg)) elif args.kwonlyargs: parts.append("*") kw_defaults = { arg.arg: "..." for arg, default in zip(args.kwonlyargs, args.kw_defaults) if default is not None } parts.extend(format_arg(arg, kw_defaults.get(arg.arg)) for arg in args.kwonlyargs) if args.kwarg: parts.append("**" + format_arg(args.kwarg)) prefix = "async def" if isinstance(node, ast.AsyncFunctionDef) else "def" returns = f" -> {unparse(node.returns)}" if node.returns is not None else "" suffix = ": ..." if one_line else ":" return f"{prefix} {node.name}({', '.join(parts)}){returns}{suffix}"

def decorators(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef) -> list[str]: return [f"@{unparse(item)}" for item in node.decorator_list]

def docstring_lines(node: ast.AST, indent: str) -> list[str]: doc = ast.get_docstring(node, clean=False) if not doc: return [] escaped = doc.replace('"""', '\"\"\"') lines = [f'{indent}"""'] lines.extend(f"{indent}{line}" for line in escaped.splitlines()) lines.append(f'{indent}"""') return lines

def module_level_items(tree: ast.Module) -> list[ast.AST]: return [node for node in tree.body if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef))]

def render_python_node(node: ast.AST, indent: str = "") -> list[str]: if isinstance(node, ast.ClassDef): bases = [unparse(base) for base in node.bases] bases.extend(f"{kw.arg}={unparse(kw.value)}" for kw in node.keywords if kw.arg) header = f"class {node.name}" if bases: header += f"({', '.join(bases)})" header += ":" lines = [(f"{indent}{dec}" for dec in decorators(node)), f"{indent}{header}"] body_lines = docstring_lines(node, indent + " ") members = [ child for child in node.body if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) ] for child in members: body_lines.extend(render_python_node(child, indent + " ")) if not body_lines: body_lines.append(indent + " ...") return lines + body_lines if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): lines = [(f"{indent}{dec}" for dec in decorators(node)), f"{indent}{format_signature(node)}"] body_lines = docstring_lines(node, indent + " ") body_lines.append(indent + " ...") lines.extend(body_lines) return lines return []

def extract_python(files: list[Path], root: Path) -> ExtractionResult: chunks: list[str] = [] warnings: list[str] = [] for path in files: try: text = path.read_text(encoding="utf-8") tree = ast.parse(text) except Exception as exc: # noqa: BLE001 warnings.append(f"Python parse failed for {rel(path, root)}: {exc}") continue items = module_level_items(tree) if not items: continue chunks.append(f"### {rel(path, root)}\n") chunks.append("python") module_doc = ast.get_docstring(tree, clean=False) if module_doc: chunks.append('"""') chunks.extend(module_doc.replace('"""', '\\"\\"\\"').splitlines()) chunks.append('"""') chunks.append("") for item in items: chunks.extend(render_python_node(item)) chunks.append("") chunks.append("") chunks.append("") return ExtractionResult("Python", "\n".join(chunks).rstrip(), warnings, len(files))

JAVA_DECLARATION_WORDS = ( "class", "interface", "enum", "record", ) JAVA_CONTROL_WORDS = { "if", "for", "while", "switch", "catch", "return", "new", "throw", "synchronized", }

def compact_java_signature(lines: list[str]) -> str: text = " ".join(line.strip() for line in lines) text = text.split("{", 1)[0].strip() return " ".join(text.split())

def java_doc_before(lines: list[str], index: int) -> list[str]: idx = index - 1 while idx >= 0 and not lines[idx].strip(): idx -= 1 if idx < 0 or not lines[idx].strip().endswith("*/"): return [] doc: list[str] = [] while idx >= 0: doc.append(lines[idx].rstrip()) if lines[idx].strip().startswith("/**"): break idx -= 1 return list(reversed(doc))

def looks_like_java_decl(text: str) -> bool: stripped = text.strip() if not stripped or stripped.startswith(("//", "", "/")): return False first = stripped.split("(", 1)[0].split() if first and first[0] in JAVA_CONTROL_WORDS: return False if any(f" {word} " in f" {stripped} " for word in JAVA_DECLARATION_WORDS): return True if "(" not in stripped or ")" not in stripped: return False before_paren = stripped.split("(", 1)[0].split() if not before_paren or before_paren[-1] in JAVA_CONTROL_WORDS: return False return len(before_paren) >= 2

def format_java_declaration(signature: str) -> str: if signature.endswith(";"): return signature if any(f" {word} " in f" {signature} " for word in JAVA_DECLARATION_WORDS): return signature + " { ... }" return signature + ";"

def extract_java(files: list[Path], root: Path) -> ExtractionResult: chunks: list[str] = [] warnings: list[str] = [] for path in files: try: lines = path.read_text(encoding="utf-8").splitlines() except Exception as exc: # noqa: BLE001 warnings.append(f"Java read failed for {rel(path, root)}: {exc}") continue decls: list[str] = [] idx = 0 annotations: list[str] = [] while idx < len(lines): stripped = lines[idx].strip() if stripped.startswith("@"): annotations.append(stripped) idx += 1 continue candidate_lines = [stripped] probe = idx while probe + 1 < len(lines) and "{" not in " ".join(candidate_lines) and not stripped.endswith(";"): probe += 1 next_line = lines[probe].strip() if not next_line or next_line.startswith(("//", "*")): break candidate_lines.append(next_line) stripped = next_line signature = compact_java_signature(candidate_lines) if looks_like_java_decl(signature): doc = java_doc_before(lines, idx) if doc: decls.extend(doc) decls.extend(annotations) decls.append(format_java_declaration(signature)) decls.append("") annotations = [] idx += 1 if decls: chunks.append(f"### {rel(path, root)}\n") chunks.append("java") chunks.extend(decls) chunks.append("") chunks.append("") return ExtractionResult("Java", "\n".join(chunks).rstrip(), warnings, len(files))

def extract_ts(files: list[Path], root: Path, script_dir: Path) -> ExtractionResult: if not files: return ExtractionResult("TypeScript / JavaScript", "", [], 0) node_script = script_dir / "extract_ts_declarations.mjs" cmd = ["node", str(node_script), "--root", str(root), *map(str, files)] try: completed = subprocess.run(cmd, text=True, capture_output=True, check=False) except FileNotFoundError: return ExtractionResult( "TypeScript / JavaScript", "", ["Node.js was not found; skipped TypeScript/JavaScript extraction."], len(files), ) if completed.returncode != 0: warning = completed.stderr.strip() or completed.stdout.strip() or "unknown error" return ExtractionResult("TypeScript / JavaScript", "", [warning], len(files)) return ExtractionResult("TypeScript / JavaScript", completed.stdout.strip(), [], len(files))

def build_markdown(root: Path, results: list[ExtractionResult], warnings: list[str]) -> str: timestamp = dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds") lines = [ "# All Declarations", "", f"- Source root: {root}", f"- Generated: {timestamp}", f"- Source files scanned: {sum(result.file_count for result in results)}", "", "This file is generated for LLM context. It preserves declarations, signatures, types, and documentation while omitting implementation bodies.", "", ] if warnings: lines.extend(["## Warnings", ""]) lines.extend(f"- {warning}" for warning in warnings) lines.append("") for result in results: if not result.markdown: continue lines.extend([f"## {result.title}", "", result.markdown, ""]) return "\n".join(lines).rstrip() + "\n"

def main() -> int: parser = argparse.ArgumentParser(description=doc) parser.add_argument("paths", nargs="*", default=["."], help="workspace directories or source files") parser.add_argument("--output", default="all-declarations.md", help="output Markdown file") parser.add_argument("--exclude", action="append", default=[], help="additional glob pattern to exclude") parser.add_argument("--no-ts", action="store_true", help="skip TypeScript/JavaScript extraction") parser.add_argument("--no-python", action="store_true", help="skip Python extraction") parser.add_argument("--no-java", action="store_true", help="skip Java best-effort extraction") args = parser.parse_args()

input_paths = [Path(path) for path in args.paths]
try:
    root, files = collect_files(input_paths, args.exclude)
except FileNotFoundError as exc:
    print(f"ERROR: {exc}", file=sys.stderr)
    return 2

script_dir = Path(__file__).resolve().parent
ts_files = [path for path in files if path.suffix.lower() in TS_EXTENSIONS and not path.name.endswith(".d.ts.map")]
py_files = [path for path in files if path.suffix.lower() in PY_EXTENSIONS]
java_files = [path for path in files if path.suffix.lower() in JAVA_EXTENSIONS]

results: list[ExtractionResult] = []
if not args.no_ts:
    results.append(extract_ts(ts_files, root, script_dir))
if not args.no_python:
    results.append(extract_python(py_files, root))
if not args.no_java:
    results.append(extract_java(java_files, root))

warnings = [warning for result in results for warning in result.warnings]
unsupported = [
    rel(path, root)
    for path in files
    if path.suffix.lower() not in TS_EXTENSIONS | PY_EXTENSIONS | JAVA_EXTENSIONS
]
if unsupported:
    warnings.append(f"Skipped unsupported source files: {len(unsupported)}")

output_path = Path(args.output)
if not output_path.is_absolute():
    output_path = root / output_path
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(build_markdown(root, results, warnings), encoding="utf-8")
print(f"Wrote {output_path}")
if warnings:
    print("Warnings:")
    for warning in warnings:
        print(f"- {warning}")
return 0

if name == "main": sys.exit(main())


# Appendix — Agent skill context-packing

context-packing/SKILL.md

name: repomix-codegen-files description: Use Repomix to analyze a workspace and produce a minimal, evidence-backed list of files needed for a code generation task. Use when asked to identify implementation, contract, config, test, or context files before generating code for a feature, bug fix, refactor, or integration. argument-hint: "[code generation task and optional workspace path]" user-invocable: true

Repomix Codegen Files

Overview

Identify the smallest useful file set for a code generation task before implementation. Use Repomix to pack repository structure, source content, and local diffs, rank likely files with a deterministic helper, then curate the final codegen-files.md list.

Use File Selection Rubric for selection rules. Use scripts/score_repomix_files.py to rank candidate files from Repomix JSON output.

Inputs

  • A concrete code generation task, such as a feature, bug fix, refactor, integration, migration, or test generation request.
  • An optional workspace path. Default to the current VS Code workspace or current shell directory.
  • Optional constraints, such as target package, framework, files to exclude, required test scope, or output path.

Ask one concise clarifying question only when the task is too vague to select files, for example "make auth better" without a product area, behavior, or failing scenario.

Safety And Setup

  1. Work only in the target workspace unless the user explicitly asks for a remote repository or another path.
  2. Check whether Repomix is available:
repomix --version
  1. If Repomix is missing, ask before installing packages or using network access. With approval, use the project's package policy or npx repomix@latest.
  2. Keep Repomix security checks enabled. Do not use --no-security-check unless the user explicitly accepts the risk for a trusted local repository.
  3. Do not include secrets, .env files, credentials, dependency folders, generated build output, or vendored code in the final file list.
  4. If Repomix reports suspicious files or security warnings, pause and report them before continuing.

Default Commands

From the target workspace root, create a local Repomix artifact:

mkdir -p .repomix
repomix --style json --output .repomix/codegen-context.json --include-diffs --top-files-len 20 --token-count-tree 100

If the user provides a narrow package or area, constrain Repomix rather than packing the whole repository:

repomix --style json --output .repomix/codegen-context.json --include "src/**,tests/**,package.json,tsconfig.json" --include-diffs --top-files-len 20

If a prior discovery step produces an exact path list, use Repomix stdin mode:

repomix --style json --output .repomix/codegen-context.json --stdin

Then rank candidates. Resolve the script path relative to this skill folder:

python3 /path/to/repomix-codegen-files/scripts/score_repomix_files.py .repomix/codegen-context.json --task "describe the code generation task" --output .repomix/codegen-candidates.md --top 60

Workflow

  1. Restate the code generation task in one sentence and note any assumptions.
  2. Resolve the workspace root. Prefer git rev-parse --show-toplevel when the target is a Git repository.
  3. Run quick discovery with git status --short and rg --files to identify changed files, languages, package boundaries, tests, and generated directories.
  4. Run Repomix with JSON output. Include diffs for active worktrees so recent changes influence selection.
  5. Run the scoring helper to produce .repomix/codegen-candidates.md.
  6. Inspect the top candidates and trace relationships with targeted rg searches for routes, symbols, imports, exports, tests, schemas, config keys, and error messages.
  7. If the first pass is too broad or misses an obvious subsystem, run a second constrained Repomix pass with --include or --ignore.
  8. Curate the final list using the rubric. Prefer a minimal set that lets a code generation agent implement and test the change without guessing.
  9. Write codegen-files.md in the workspace root unless the user requested inline output or another path.
  10. Validate that every listed path exists, is relevant to the task, and is not generated, vendored, secret-bearing, or merely interesting background.

Output Contract

codegen-files.md must contain:

  • Task summary and workspace path.
  • Repomix command or artifact path used as evidence.
  • Required edit files, with path, role, reason, evidence, and confidence.
  • Context-only files needed to understand contracts, call sites, schemas, routing, or configuration.
  • Test and fixture files needed to validate the generated code.
  • Optional follow-up files that may be needed after inspecting implementation details.
  • Excluded near-misses when they explain scope decisions.
  • Commands run and any Repomix warnings or fallback limitations.

Do not start generating code unless the user explicitly asks to continue after the file list is produced.

Fallback Behavior

If Repomix cannot run and the user does not approve installation or network access, produce a fallback list from rg --files, git status --short, targeted source inspection, and dependency tracing. Label the output Repomix unavailable fallback and explain what confidence was lost.

Examples

/repomix-codegen-files "Add SSO login to the admin web app"
/repomix-codegen-files packages/api "Fix project membership authorization after role changes"
/repomix-codegen-files "Generate tests for the invoice retry scheduler; exclude docs and build output"

context-packing/references/file-selection-rubric.md

File Selection Rubric

Goal

Produce a compact, task-specific file list that a code generation agent can use to implement the requested change without guessing about interfaces, behavior, tests, or configuration.

The list should be large enough to preserve correctness and small enough to avoid drowning the code generation task in unrelated context.

Selection Categories

Use these categories in the final codegen-files.md:

  • Required edit files: files that almost certainly need code changes.
  • Contract files: types, interfaces, schemas, API definitions, protocol files, migrations, or configuration that define valid behavior.
  • Callers and entrypoints: routes, commands, controllers, jobs, UI screens, package exports, or registration files that connect the feature to runtime behavior.
  • Tests and fixtures: existing tests, fixtures, snapshots, mocks, test factories, or integration harnesses that should be updated or mirrored.
  • Context-only files: files needed to understand patterns but probably not edit.
  • Optional follow-up files: files to inspect after implementation reveals details.
  • Excluded near-misses: plausible files deliberately omitted, with the reason.

Evidence Signals

Prefer files with direct evidence:

  • Path or symbol names match the task terms.
  • Repomix content contains relevant routes, commands, UI labels, error messages, config keys, schema names, or domain terms.
  • The file appears in the local diff for active work related to the task.
  • Imports, exports, registrations, or route tables connect the file to a selected edit target.
  • Tests mention the same behavior, endpoint, component, fixture, or failure case.
  • Package manifests, build config, feature flags, or environment config control the code path.

Treat the following as weaker signals:

  • Same broad folder with no matching symbols or imports.
  • Similar names in unrelated packages.
  • Documentation-only mentions when the task asks for implementation.
  • Large framework files that are interesting but not task-specific.

Minimality Rules

  1. Include a file only when it has a clear role in implementing, compiling, wiring, or validating the requested change.
  2. Prefer direct contracts over transitive implementation details when the code generator only needs the API shape.
  3. Prefer nearby tests over every test in the package.
  4. Include package-level config only when it affects the target runtime, build, type checking, test runner, routing, code generation, or dependency graph.
  5. Exclude generated files, dependency directories, lockfiles, minified bundles, snapshots, and binary artifacts unless the task specifically targets them.
  6. Avoid including secrets or files likely to contain credentials. If a secret-bearing file seems relevant, describe the needed setting without listing the file contents.

Confidence Labels

  • High: direct edit target or directly referenced by a required target, with matching symbols or tests.
  • Medium: likely needed based on package structure, routing, contracts, or config, but not guaranteed.
  • Low: useful to inspect only if implementation details require it.

Requirement Gaps

Ask a clarifying question before producing the final list when any of these materially changes the result:

  • The target application, package, service, or language is unknown in a multi-project workspace.
  • The user asks for a feature but not the user-visible behavior, API, or failing case.
  • The task could refer to multiple similarly named subsystems.
  • The user expects generated code but has not said whether tests, migrations, or docs are in scope.
  • Repomix cannot run and fallback confidence would be too low.

Otherwise, proceed with stated assumptions.

Report Template

# Codegen Files

Task: ...
Workspace: ...
Repomix evidence: ...
Generated: ...

## Required Edit Files

| Path | Role | Why Needed | Evidence | Confidence |
| --- | --- | --- | --- | --- |
| `src/example.ts` | Implementation | ... | ... | High |

## Contract Files

| Path | Role | Why Needed | Evidence | Confidence |
| --- | --- | --- | --- | --- |

## Callers And Entrypoints

| Path | Role | Why Needed | Evidence | Confidence |
| --- | --- | --- | --- | --- |

## Tests And Fixtures

| Path | Role | Why Needed | Evidence | Confidence |
| --- | --- | --- | --- | --- |

## Context-Only Files

| Path | Role | Why Needed | Evidence | Confidence |
| --- | --- | --- | --- | --- |

## Optional Follow-Up Files

- `path/to/file`: inspect if ...

## Excluded Near-Misses

- `path/to/file`: excluded because ...

## Commands Run

```bash
...
```

## Notes

- ...

context-packing/scripts/score_repomix_files.py

!/usr/bin/env python3

"""Rank files from Repomix JSON for a code generation task.

The output is a candidate list, not the final answer. The agent should inspect the ranked files and curate a minimal, evidence-backed codegen-files.md. """

from future import annotations

import argparse import json import re import sys from dataclasses import dataclass from pathlib import Path

STOP_WORDS = { "about", "after", "again", "against", "all", "also", "and", "any", "app", "are", "around", "before", "bug", "build", "can", "change", "code", "codegen", "codegeneration", "create", "fix", "for", "from", "generate", "generated", "generation", "get", "has", "have", "into", "make", "need", "needed", "new", "not", "now", "out", "refactor", "related", "should", "task", "test", "tests", "that", "the", "this", "through", "to", "update", "use", "uses", "using", "when", "with", "without", }

SOURCE_EXTENSIONS = { ".c", ".cc", ".clj", ".cljs", ".cpp", ".cs", ".css", ".dart", ".ex", ".exs", ".go", ".h", ".hpp", ".html", ".java", ".js", ".jsx", ".kt", ".kts", ".lua", ".m", ".mm", ".php", ".py", ".rb", ".rs", ".scala", ".scss", ".sh", ".svelte", ".swift", ".tsx", ".ts", ".vue", }

CONFIG_NAMES = { "angular.json", "babel.config.js", "cargo.toml", "composer.json", "dockerfile", "eslint.config.js", "go.mod", "jest.config.js", "next.config.js", "package.json", "pom.xml", "pyproject.toml", "requirements.txt", "rollup.config.js", "setup.py", "tsconfig.json", "vite.config.js", "webpack.config.js", }

GENERATED_PARTS = { ".git", ".next", ".nuxt", ".repomix", ".turbo", ".venv", "pycache", "build", "coverage", "dist", "node_modules", "out", "target", "vendor", }

@dataclass(frozen=True) class ScoredFile: path: str score: float kind: str chars: int reasons: tuple[str, ...]

def extract_path_hints(task: str) -> list[str]: hints = [] for match in re.findall(r"[\w./-]+.[A-Za-z0-9]{1,8}", task): cleaned = match.strip(".,;:'\"()[]{}") if "/" in cleaned or "." in cleaned: hints.append(cleaned.lower()) return sorted(set(hints), key=str.lower)

def normalize_terms(text: str) -> list[str]: expanded = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", text) rawterms = re.findall(r"[A-Za-z0-9]+", expanded.replace("", " ").replace("-", " ")) terms = [] for term in raw_terms: lowered = term.lower() if len(lowered) < 3 or lowered in STOP_WORDS: continue terms.append(lowered) return sorted(set(terms), key=str.lower)

def load_repomix_json(path: Path) -> dict[str, str]: try: data = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: raise SystemExit(f"Could not parse Repomix JSON: {exc}") from exc

files = data.get("files")
if not isinstance(files, dict):
    raise SystemExit("Repomix JSON does not contain a top-level object named 'files'.")

normalized: dict[str, str] = {}
for file_path, content in files.items():
    if not isinstance(file_path, str):
        continue
    if content is None:
        normalized[file_path] = ""
    elif isinstance(content, str):
        normalized[file_path] = content
    else:
        normalized[file_path] = json.dumps(content, sort_keys=True)
return normalized

def file_kind(path: str) -> str: p = path.lower() parts = set(Path(p).parts) name = Path(p).name suffix = Path(p).suffix

if parts & GENERATED_PARTS or ".min." in name:
    return "generated-or-vendor"
if (
    "/test/" in f"/{p}/"
    or "/tests/" in f"/{p}/"
    or "__tests__" in p
    or ".test." in name
    or ".spec." in name
    or name.startswith("test_")
    or name.endswith("_test.py")
):
    return "test"
if name in CONFIG_NAMES or name.endswith((".config.js", ".config.ts", ".config.mjs", ".config.cjs")):
    return "config"
if suffix in SOURCE_EXTENSIONS:
    return "source"
if suffix in {".graphql", ".proto", ".sql", ".toml", ".yaml", ".yml", ".json"}:
    return "contract-or-config"
if suffix in {".md", ".mdx", ".rst"}:
    return "docs"
return "support"

def countterm(content: str, term: str) -> int: pattern = re.compile(rf"(?<![A-Za-z0-9]){re.escape(term)}(?![A-Za-z0-9_])", re.IGNORECASE) return len(pattern.findall(content))

def score_file(path: str, content: str, terms: list[str], path_hints: list[str]) -> ScoredFile: lower_path = path.lower() lower_name = Path(lower_path).name kind = file_kind(path) score = 0.0 reasons: list[str] = []

matched_hints = [hint for hint in path_hints if hint in lower_path]
if matched_hints:
    score += 30 + (6 * len(matched_hints))
    reasons.append("matches path hint " + ", ".join(matched_hints[:4]))

path_matches = [term for term in terms if term in lower_path]
if path_matches:
    score += 12 * len(path_matches)
    score += 6 * sum(1 for term in path_matches if term in lower_name)
    reasons.append("path terms " + ", ".join(path_matches[:8]))

content_matches: list[tuple[str, int]] = []
for term in terms:
    hits = count_term(content, term)
    if hits:
        content_matches.append((term, hits))
        score += min(18, hits * 2)

if content_matches:
    compact = ", ".join(f"{term}({hits})" for term, hits in sorted(content_matches, key=lambda item: (-item[1], item[0]))[:8])
    reasons.append("content terms " + compact)

has_task_match = bool(matched_hints or path_matches or content_matches)
if has_task_match:
    if kind == "source":
        score += 5
        reasons.append("source file")
    elif kind == "test":
        score += 4
        reasons.append("test file")
    elif kind in {"config", "contract-or-config"}:
        score += 3
        reasons.append(kind)
    elif kind == "docs":
        score -= 2
        reasons.append("docs-only candidate")
    elif kind == "generated-or-vendor":
        score -= 40
        reasons.append("generated or vendor path")

if len(content) > 250_000 and has_task_match:
    score -= 5
    reasons.append("large file")

if not has_task_match:
    score = 0.0
    reasons = []

return ScoredFile(path=path, score=score, kind=kind, chars=len(content), reasons=tuple(reasons))

def md_escape(value: object) -> str: text = str(value) return text.replace("|", "\|").replace("\n", " ")

def build_markdown( repomix_json: Path, task: str, terms: list[str], path_hints: list[str], scores: list[ScoredFile], analyzed_count: int, top: int, min_score: float, ) -> str: lines = [ "# Repomix Codegen Candidate Files", "", f"Task: {task}", f"Repomix JSON: {repomix_json}", f"Files analyzed: {analyzed_count}", f"Task terms: {', '.join(terms) if terms else 'none extracted'}", f"Path hints: {', '.join(path_hints) if path_hints else 'none'}", f"Minimum score: {min_score:g}", "", "Use this candidate inventory as evidence, then curate the final codegen-files.md with required edit files, contracts, entrypoints, tests, context-only files, and excluded near-misses.", "", ]

selected = [item for item in scores if item.score >= min_score][:top]
if not selected:
    lines.extend(
        [
            "No candidates met the score threshold.",
            "",
            "Try a more specific task description, inspect `rg --files`, or rerun Repomix with a narrower `--include` pattern.",
        ]
    )
    return "\n".join(lines) + "\n"

lines.extend(
    [
        "| Rank | Path | Score | Kind | Size | Why |",
        "| --- | --- | ---: | --- | ---: | --- |",
    ]
)
for rank, item in enumerate(selected, start=1):
    reason = "; ".join(item.reasons) if item.reasons else "task-adjacent"
    lines.append(
        f"| {rank} | `{md_escape(item.path)}` | {item.score:.1f} | {md_escape(item.kind)} | {item.chars} | {md_escape(reason)} |"
    )

lines.extend(
    [
        "",
        "## Review Checklist",
        "",
        "- Verify every high-scoring path exists in the workspace.",
        "- Trace imports, exports, routes, commands, schemas, and tests around the top candidates.",
        "- Add nearby tests or fixtures even when they score below implementation files.",
        "- Exclude generated, vendored, secret-bearing, and unrelated documentation files.",
        "- Mark final confidence as High, Medium, or Low based on direct evidence.",
    ]
)
return "\n".join(lines) + "\n"

def parse_args(argv: list[str]) -> argparse.Namespace: parser = argparse.ArgumentParser(description=doc) parser.add_argument("repomix_json", type=Path, help="Path to Repomix JSON output.") parser.add_argument("--task", required=True, help="Code generation task description.") parser.add_argument("--output", type=Path, help="Markdown output path. Defaults to stdout.") parser.add_argument("--top", type=int, default=50, help="Maximum candidates to print.") parser.add_argument("--min-score", type=float, default=1.0, help="Minimum candidate score.") return parser.parse_args(argv)

def main(argv: list[str]) -> int: args = parse_args(argv) files = load_repomix_json(args.repomix_json) terms = normalize_terms(args.task) path_hints = extract_path_hints(args.task) scores = sorted( (score_file(path, content, terms, path_hints) for path, content in files.items()), key=lambda item: (-item.score, item.path.lower()), )

markdown = build_markdown(
    repomix_json=args.repomix_json,
    task=args.task,
    terms=terms,
    path_hints=path_hints,
    scores=scores,
    analyzed_count=len(files),
    top=args.top,
    min_score=args.min_score,
)
if args.output:
    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(markdown, encoding="utf-8")
else:
    print(markdown, end="")
return 0

if name == "main": raise SystemExit(main(sys.argv[1:]))


메타데이터
post_id
c71f99c02550
slug
token-economy-with-sdd-how-to-burn-gpus-the-right-way-c71f99c02550
url
https://medium.com/@atiurrs/token-economy-with-sdd-how-to-burn-gpus-the-right-way-c71f99c02550
canonical_url
https://medium.com/@atiurrs/token-economy-with-sdd-how-to-burn-gpus-the-right-way-c71f99c02550
author_url
https://medium.com/@atiurrs
status
ok
fetched_at
2026-06-13 07:35:29