diff --git a/.agents/skills/exa-web-search/SKILL.md b/.agents/skills/exa-web-search/SKILL.md new file mode 100644 index 00000000..4af93900 --- /dev/null +++ b/.agents/skills/exa-web-search/SKILL.md @@ -0,0 +1,207 @@ +--- +name: exa-web-search +description: "Deep research powered by Exa. Use for lead generation, literature reviews, deep dives, competitive analysis, or any query where one search falls short, including phrases like 'research this', 'find everything about', 'find me all', or 'deep dive on'." +--- + +# Exa Research Orchestrator + +You are the orchestrator. Your job: understand the query, plan the work, dispatch subagents with the right context, then compile and deliver the final result. + +## Prerequisites: Auth + +Server: `https://mcp.exa.ai/mcp`. + +1. **OAuth (recommended)** — client opens `auth.exa.ai`, user signs in with Google / SSO / email, JWT is attached automatically. No key to copy. +2. **API key** — if OAuth isn't available, get one at https://dashboard.exa.ai/api-keys and pass it via `Authorization: Bearer …`, `?exaApiKey=…`, or `EXA_API_KEY` (local npm). +3. **Anonymous** — works without setup but rate-limited. + +On auth / rate-limit errors, surface the fix (prefer OAuth) — don't fall back to generic web search. + +## Date Calculation (Do This First) + +If the query involves time ("last week", "recent", "past 6 months"), calculate exact dates from today's date in your environment context. Write out the calculation explicitly before doing anything else. Never eyeball dates or reuse dates from examples. + +## Step 1: Assess the Query + +Read the user's query and determine two things: + +**How complex is this?** +- **Extremely Simple** (e.g. reading the contents of 1-2 pages): Handle it yourself. Read `references/searching.md` for query-writing guidance, run the searches, review and filter results, then respond directly. No subagents needed. +- **Moderate** (when a fast or low-effort search is requested): Delegate to 1 subagent to keep your context window clean. +- **Advanced** (clear topic, clear filters, a few parallel searches): Light subagent use. One round of parallel subagents, then compile. +- **Complex** (cross-referencing across entity types, multi-hop chains, exhaustive coverage, semantic filtering): Full multi-pass with parallel subagents. + +**Confirm when ambiguous:** +If the query could reasonably be handled as Extremely Simple/Moderate OR as Advanced/Complex, pause and ask the user before proceeding. Present: +1. Your interpretation of the query +2. The two (or more) plausible complexity levels +3. What each level would look like in practice (e.g., "I can do a quick 1-2 search lookup, or I can fan out across 3-4 subagents to get deeper coverage") +4. Let the user choose + +Examples of ambiguous queries: +- "What are the best LLM fine-tuning frameworks?" — could be a quick opinionated list (Moderate) or an exhaustive evaluated comparison (Complex) +- "Find competitors to Acme Corp" — could be a quick search for known competitors (Moderate) or a deep sweep across funding databases, press, and niche directories (Complex) +- "What's the latest on WebGPU?" — could be one news search (Extremely Simple) or a multi-angle survey of specs, browser support, community adoption, and benchmarks (Advanced) + +Do NOT ask for confirmation when: +- The query is clearly extremely simple (fact lookups, single-entity questions) +- The query is clearly complex (explicit multi-constraint, "find everything", "exhaustive", "comprehensive") +- The user has already specified depth ("do a deep dive", "quick answer") + +Note: if the user explicitly asks for something (e.g. "100" of something), continue to work until you've achieved it. + +**What work needs to happen?** Identify which of these apply (most queries use 3-5): + +1. **Seed from user input**: The user provided a list of entities to start from (company names, tickers, paper titles). Each seed becomes a parallel workstream. +2. **Define what qualifies**: What makes a result a valid "row"? Translate the user's criteria into concrete checks. +3. **Define what to capture**: What fields ("columns") does each result need? Build the schema before searching. +4. **Search broadly**: Generate diverse queries and run them to find candidates. This is where subagents do the heavy lifting. +5. **Extract structured data**: Pull specific fields from raw search results into the schema. +6. **Filter**: Apply hard constraints (dates, geography, thresholds) and soft judgments (quality, relevance, semantic checks). +7. **Merge and deduplicate**: Combine results from multiple subagents. Same URL = drop duplicate. Same entity from different sources = merge fields, keep best data. +8. **Score and rank**: For "best of" (e.g. "what's the best ___?") queries, define the scoring criteria explicitly, then rank. +9. **Synthesize narrative**: For research queries, organize findings by theme and write prose with citations. + +## Step 2: Dispatch Subagents + +### What subagents do + +Subagents run Exa searches and process the results. They keep raw search output out of your context window. Each subagent should: +- Read the reference file(s) you point it to +- Run the specific searches you assign +- Return compact, structured output + +### How to dispatch + +Use the **Agent tool** to dispatch subagents. Reference file paths are relative to the directory this file was loaded from. + +Use `model: "haiku"` for subagents. + +Tell each subagent: +1. Which reference file(s) to read for instructions (always include the absolute path) +2. What specific searches to run or what specific work to do +3. What output format to return + +**Template:** +``` +Read the file at [this skill's directory]/references/searching.md for instructions on how to query Exa effectively. + +Then do the following: +[specific task description] +[specific queries to run, if you are prescribing them] +[validation criteria -- what makes a result qualify, so the subagent filters before returning] + +Return: [output format -- e.g. "compact JSON with name, url, snippet per result" or "markdown table with columns X, Y, Z"]. + +End with EXACTLY: `sources_reviewed: N` where N = sum of `numResults` across every `web_search_exa` call (incl. retries). E.g. calls with numResults 10, 10, 5 → `sources_reviewed: 25`. +``` + +**Pass the `sources_reviewed` instruction line to every subagent verbatim — don't paraphrase.** +### Which reference files to point subagents to + +Always point subagents to `references/searching.md`. It contains Exa query guidance and an index of domain-specific pattern files that the subagent will select from based on its task. + +Point to whichever of these also apply: + +| File | Point a subagent here when... | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------- | +| `references/extraction.md` | The subagent needs to extract specific data points into a schema you defined | +| `references/filtering.md` | The subagent needs to evaluate results against criteria (especially semantic/soft filters) | +| `references/synthesis.md` | The subagent is producing a prose synthesis rather than structured data | +| `references/source-quality.md` | The subagent needs to assess source credibility, especially for "best of", ranking, or expert-finding queries | + +### How to split work across subagents + +If running parallel subagents, decompose the primary task/question into **sub-questions** to cover different search territories. + +For example, "best open-source LLM fine-tuning frameworks for production use" can be decomposed into multiple parallel sub-questions: +1. "What open-source LLM fine-tuning frameworks do production engineers recommend, and what do they say about using them in real deployments?" +2. "What open-source LLM fine-tuning tools have launched or gained traction in the last 6 months that aren't yet widely known?" +3. "What are the most common complaints, failure modes, and reasons teams migrated away from specific open-source LLM fine-tuning frameworks in production?" + +Depending on your "**How complex is this?**" analysis: Some need 2-3; some need many. Some need several different angles, creative thought patterns, adversarial perspectives. It depends on what the user is asking for and how deep they want you to go. + +Give the sub-question directly to the subagent in its prompt. + +### Subagent sizing + +- Aim for 3-5 searches per subagent +- Parallelize aggressively — independent workstreams should be separate subagents launched in a single message +- Do not use `run_in_background` — dispatch all subagents in one message and wait for their results +- For per-seed work (enriching a list of 20 companies), batch 3-5 seeds per subagent + +### Token isolation + +Never run bulk searches in your main context. The whole point of subagents is to keep raw search output out of your context window. Subagents process results and return only distilled output. + +### When things go wrong + +- **Subagent returns empty**: Rephrase queries with different angles, not synonyms. If still empty, the topic may have limited web coverage -- report that. +- **Subagent returns off-topic results**: Queries were too vague. Retry with longer, more specific queries. + +## Step 3: Compile Results + +After subagents return: + +**Deduplicate:** +1. Collect all results into a single list +2. Remove exact URL duplicates +3. Same entity from different sources: merge fields, keep the most complete/recent data +4. Track: "Deduplicated X results down to Y unique entries" + +**Validate coverage:** +- Are there obvious gaps? (missing time periods, missing geographic regions, missing entity types) +- For each gap found, run targeted follow-up searches (via subagent if multiple queries are needed, direct if extremely simple) +- For "find everything" queries, check if results from different subagents overlap heavily (good sign) or are completely disjoint (may indicate missed angles) + +**Format the output:** + +If you used subagents, open with: "I used Exa to review {X} sources across {Y} subagents. Here's what was found:" (X = sum of `sources_reviewed` across all subagents and passes plus any direct searches you ran; Y = total subagents dispatched. Pluralize naturally.) + +Then: Format output beautifully, filling up no more than one scroll length of the claude code screen. Include hyperlinked text where relevant. Below it, you may also include things (in a short, easy-to-read format) that: +- ("Result") directly answer the original user request (in few words; make every word count) +- ("Process") include anything worth noting about your process and what you consider to be high-signal in this domain vs. what you filtered out. +- ("Patterns") any patterns identified that are non-obvious, require n-th order thinking, and are not included or alluded to in the rest of the output but might be interesting to the user. +- ("Notes") based on everything you know about the user and their work beyond this task, mention anything notable/useful you found that is not included or alluded to in the rest of the output. + +If it's impossible to fit the full output in a single screen, write a file in the most relevant/useful file format (.csv, .md) to `./exa-results/-` and include a pointer to the full file below the 1-screen output. + +**General output rules:** +- No emojis unless the user requested them +- Include in-line 1-word or multi-word hyperlinks throughout outputs where hyperlinking is a value-add. +- Prefer tables over lists (fall back to lists only when fields are non-uniform or values are too long to fit cleanly) + +## Multi-Pass Queries + +Some queries require multiple sequential passes where later passes depend on earlier results. Common patterns: + +**Entity chaining** (multi-hop): Pass 1 finds entities (companies), Pass 2 finds related entities per result (people at those companies), Pass 3 enriches those (their public statements). Each pass is a round of parallel subagents. + +**Exploratory then targeted**: Pass 1 scouts the landscape broadly, Pass 2 searches deeply in the most promising directions found in Pass 1. + +**Criteria discovery**: When "best" isn't predefined, Pass 1 surveys what practitioners actually value, Pass 2 searches for candidates matching those criteria. + +Between passes, compile and deduplicate before dispatching the next round. + +## Evaluating Source Quality + +Source quality matters most for "best of", ranking, expert-finding, and best-practices queries, but is useful context for almost any research task. + +**At the subagent level:** Point subagents to `references/source-quality.md` so they tag source quality in their output. This lets you weight results during compilation. + +**At the orchestrator level**, when compiling subagent results: + +1. **Convergence across high-signal sources**: Convergence alone isn't meaningful (3 low-quality sources agreeing is just shared noise). What matters is when multiple independent, high-signal sources (practitioners, people with skin in the game) converge on the same finding. +2. **Practitioner vs commentator**: Weight practitioners (people doing the work) higher than commentators (people writing about the work). +3. **Via negativa**: Before synthesizing, define who to exclude (sources with misaligned incentives, no skin in the game, or unfalsifiable claims). Filtering out noise is more valuable than seeking brilliance. +4. **Red-team your compiled results**: What perspectives are missing? What biases might be distorting the aggregate? If a gap emerges, run a targeted follow-up. +5. **Ideas over entities**: For expert-finding and best-practices queries, the primary output is convergent truths, not a ranked list of names. Lead with what the best sources agree on, then cite who said it. + +## Gotchas + +- **Over-execution on simple queries**: If the user asks "what year was X founded", don't spin up subagents. One search, one answer. +- **Under-execution on hard queries**: If the query has 4+ constraints, temporal joins, or semantic filtering, a single search will not cut it. Fan out. +- **Synonym queries**: Running "overrated AI tools" and "overhyped AI tools" as separate subagent queries wastes tokens. These hit the same embedding region. Diversify by angle instead. +- **Forgetting to deduplicate**: Multiple subagents will return overlapping results. Always deduplicate before synthesis. +- **Treating Exa results as validated**: Exa returns similarity, not yet validated. A result appearing in search output does not mean it meets the user's criteria. You must validate. +- **Date drift**: Always calculate dates from the current environment date. Never reuse dates from these instructions or from previous queries. diff --git a/.agents/skills/exa-web-search/references/extraction.md b/.agents/skills/exa-web-search/references/extraction.md new file mode 100644 index 00000000..64167825 --- /dev/null +++ b/.agents/skills/exa-web-search/references/extraction.md @@ -0,0 +1,63 @@ +# Extracting Structured Data from Search Results + +After running searches, you need to extract structured information from the results. This file covers how to do that well. + +## When You Have Enough from Snippets + +Exa search results include titles, URLs, and text snippets ("highlights"). For many fields (company name, person name, funding round, publication date), the snippet is sufficient. Extract directly from what you have before fetching full pages. + +## When to Deep-Read with web_fetch_exa + +Fetch the full page when: +- The snippet mentions what you need but doesn't include the actual value +- You need to read body text to make a judgment call (e.g. "does this blog post show genuine design opinion or is it generic?") +- You need to extract multiple fields from a single rich source (case study page, team page, filing) +- The task requires reading beyond the first few sentences + +``` +web_fetch_exa { + "urls": ["https://source-1.com", "https://source-2.com"], +} +``` + +Batch up to 5-10 URLs per fetch call to minimize round trips. Avoid using `maxCharacters` param or `head`/`tail` bash tools; the point is to understand full page context. + +## Extracting into a Schema + +When you've been given a schema (the "columns" for the result), extract each field per result: + +1. **Structured fields** (name, date, URL, funding amount, ticker): Extract the literal value. If not present, mark as missing rather than guessing. + +2. **Categorical fields** (industry, stage, role level): Map to the closest category. Note uncertainty if the mapping is ambiguous. + +3. **Semantic fields** (sentiment, whether something qualifies as "genuine opinion", relevance to a theme): Read the content and make a judgment call. Include a brief rationale so downstream synthesis can weigh your assessment. + +4. **Negation fields** ("no review mentions X", "no Series A announcement"): These require checking that something is absent. Search for the positive case; if nothing surfaces, report absence with confidence level based on how thorough your coverage was. + +## Handling Missing Data + +- Mark fields as "not found" rather than guessing or leaving blank +- Distinguish "confirmed absent" (searched thoroughly, not there) from "not found" (didn't have access or coverage was limited) +- If a source is paywalled or inaccessible, note that explicitly + +## Confidence Signals + +When extracting, note the strength of the evidence: +- **Direct**: The source explicitly states the value (e.g. "We raised $20M in Series B") +- **Inferred**: The value is derived from context (e.g. headcount estimated from team page photos) +- **Uncertain**: Single indirect signal, could be wrong + +## Output Format + +Return extracted data as compact structured output. For lists of entities: + +``` +[ + { "name": "...", "field_1": "...", "field_2": "...", "source": "url", "confidence": "direct" }, + ... +] +``` + +Or as a markdown table if that better suits your task's instructions. + +Keep output compact. Your results will be merged with results from other searches, so verbosity at this stage compounds. diff --git a/.agents/skills/exa-web-search/references/filtering.md b/.agents/skills/exa-web-search/references/filtering.md new file mode 100644 index 00000000..afa4c081 --- /dev/null +++ b/.agents/skills/exa-web-search/references/filtering.md @@ -0,0 +1,48 @@ +# Filtering Results + +After extracting data from search results, you may need to filter rows based on criteria from the original query. This file covers how to apply filters effectively. + +## Hard Filters + +Hard filters have clear, binary criteria: a date range, a geographic constraint, a numeric threshold, a category membership. + +Apply these mechanically: +- Check each row against the criterion +- Remove rows that fail +- No judgment call needed + +Examples: "published in 2025", "based in SF or NYC", "under $500B market cap", "excluding Novo Nordisk" + +**Negation filters** ("excluding X", "not sponsored by Y") are hard filters applied in reverse. Check for the presence of the excluded value and remove matches. + +## Soft Filters + +Soft filters require judgment: "genuine design opinion" vs "generic blog post", "actually shipping" vs "just evaluating", "high-signal" vs "noise". + +For these: +1. Read the relevant content (use `web_fetch_exa` if snippets are insufficient) +2. Make a judgment call based on the content +3. Include a brief rationale for each keep/drop decision so your reasoning is visible + +**Semantic negation** is a type of soft filter: "no review mentions smell, noise, or pest complaints" requires reading review content and detecting whether these topics appear, even if phrased differently. + +## Filter Order + +Apply filters in this order to minimize wasted work: +1. **Hard filters first** -- cheap, mechanical, eliminates rows before you spend tokens on judgment +2. **Soft filters second** -- only on rows that passed hard filters + +## Temporal Filters + +Queries often involve time: "in the last 6 months", "began enrolling in 2025", "recent". + +- Calculate exact date boundaries from the current date before filtering +- Check publication/event dates against the boundary +- If a date is ambiguous (e.g. "early 2025"), note the uncertainty rather than silently including or excluding + +## Completeness vs Precision + +The original query determines the balance: +- "Find every..." or "exhaustive" -- err on the side of including borderline cases, flag them as uncertain +- "Find the best..." or "top N" -- err on the side of precision, drop borderline cases +- Default: include borderline cases with a flag, let downstream processing decide diff --git a/.agents/skills/exa-web-search/references/patterns-code.md b/.agents/skills/exa-web-search/references/patterns-code.md new file mode 100644 index 00000000..3204646c --- /dev/null +++ b/.agents/skills/exa-web-search/references/patterns-code.md @@ -0,0 +1,16 @@ +# Query Patterns: Code and Documentation + +Always include programming language and framework/library in your query. + +``` +// API usage +web_search_exa { "query": "Stripe API create subscription Node.js code example", "numResults": 5 } + +// Error resolution +web_search_exa { "query": "React hydration mismatch server client explanation fix", "numResults": 10 } + +// GitHub implementations +web_search_exa { "query": "GitHub repository [library] example project open source", "numResults": 10 } +``` + +Use `web_fetch_exa` to read official docs when you know the URL. diff --git a/.agents/skills/exa-web-search/references/patterns-companies.md b/.agents/skills/exa-web-search/references/patterns-companies.md new file mode 100644 index 00000000..25ba8f3d --- /dev/null +++ b/.agents/skills/exa-web-search/references/patterns-companies.md @@ -0,0 +1,27 @@ +# Query Patterns: Companies + +Use `category:company` for structured company data (funding, headcount, description). + +``` +// By category +web_search_exa { "query": "category:company AI infrastructure startups San Francisco", "numResults": 10 } + +// By stage +web_search_exa { "query": "category:company Series B fintech payments", "numResults": 10 } + +// Similar to known company +web_search_exa { "query": "category:company companies like Stripe", "numResults": 8 } +``` + +For competitive intelligence, layer multiple angles: +``` +web_search_exa { "query": "category:company companies like [target]", "numResults": 10 } +web_search_exa { "query": "category:company [category] software tools", "numResults": 15 } +web_search_exa { "query": "[category] startup launch funding announcement recently", "numResults": 15 } +``` + +For funding/investors: +``` +web_search_exa { "query": "[company] funding round raised investors", "numResults": 5 } +web_search_exa { "query": "category:company [company]", "numResults": 5 } +``` diff --git a/.agents/skills/exa-web-search/references/patterns-news.md b/.agents/skills/exa-web-search/references/patterns-news.md new file mode 100644 index 00000000..328348cc --- /dev/null +++ b/.agents/skills/exa-web-search/references/patterns-news.md @@ -0,0 +1,12 @@ +# Query Patterns: News and Recent Events + +``` +web_search_exa { "query": "category:news [topic] announcement", "numResults": 15 } +web_search_exa { "query": "[topic] news update latest development [month year]", "numResults": 15 } +``` + +For reactions/sentiment on recent events, search across platforms: +``` +web_search_exa { "query": "[event] reaction analysis commentary", "numResults": 12 } +web_search_exa { "query": "[event] criticism concerns issues bugs", "numResults": 15 } +``` diff --git a/.agents/skills/exa-web-search/references/patterns-papers.md b/.agents/skills/exa-web-search/references/patterns-papers.md new file mode 100644 index 00000000..b0aa805f --- /dev/null +++ b/.agents/skills/exa-web-search/references/patterns-papers.md @@ -0,0 +1,19 @@ +# Query Patterns: Research Papers + +Use `category:research paper` for Exa's paper index. + +``` +// By topic +web_search_exa { "query": "category:research paper sparse attention mechanisms for long context transformers", "numResults": 12 } + +// Survey/review papers +web_search_exa { "query": "category:research paper [topic] comprehensive survey review", "numResults": 10 } + +// By author +web_search_exa { "query": "category:research paper [author name] [topic]", "numResults": 5 } + +// By recency (encode time in query) +web_search_exa { "query": "category:research paper large language model advances 2025 2026", "numResults": 15 } +``` + +To find seminal papers: search for survey papers first, then deep-read them to extract foundational references. diff --git a/.agents/skills/exa-web-search/references/patterns-people.md b/.agents/skills/exa-web-search/references/patterns-people.md new file mode 100644 index 00000000..9fa45a57 --- /dev/null +++ b/.agents/skills/exa-web-search/references/patterns-people.md @@ -0,0 +1,30 @@ +# Query Patterns: People + +Use `category:people` for LinkedIn-weighted results. For discovery queries, be specific -- vague queries like `"category:people researcher founder CEO startup"` will match many irrelevant LinkedIn profiles. Include specific companies, timeframes, or roles to narrow results. + +``` +// By company + role +web_search_exa { "query": "category:people engineer at OpenAI", "numResults": 10 } +web_search_exa { "query": "category:people VP director at Cursor", "numResults": 10 } + +// By role + location +web_search_exa { "query": "category:people Head of Growth B2B SaaS startup San Francisco", "numResults": 12 } + +// Specific person +web_search_exa { "query": "category:people Jane Smith Anthropic machine learning", "numResults": 5 } +``` + +For comprehensive company coverage, search by department and seniority in parallel: +``` +web_search_exa { "query": "category:people engineering at Acme", "numResults": 10 } +web_search_exa { "query": "category:people product design at Acme", "numResults": 10 } +web_search_exa { "query": "category:people sales marketing at Acme", "numResults": 10 } +``` + +Supplement with non-LinkedIn sources: +``` +web_search_exa { "query": "Acme team page employees about us", "numResults": 5 } +web_search_exa { "query": "joined Acme recently hired new role announcement", "numResults": 5 } +``` + +Deduplicate by LinkedIn URL (canonical) or name + current company (fallback). diff --git a/.agents/skills/exa-web-search/references/patterns-relationships.md b/.agents/skills/exa-web-search/references/patterns-relationships.md new file mode 100644 index 00000000..875c78f8 --- /dev/null +++ b/.agents/skills/exa-web-search/references/patterns-relationships.md @@ -0,0 +1,29 @@ +# Query Patterns: Hidden Relationships + +Finding connections that aren't explicitly listed anywhere. Direct queries ("X clients") return articles about them, not actual connections. Use indirect signals instead. + +**Start with the subject's own platforms:** +``` +web_search_exa { "query": "[subject] official website blog podcast", "numResults": 5 } +web_search_exa { "query": "[subject] conversation interview testimonial guest", "numResults": 8 } +web_fetch_exa { "urls": ["https://subject-website.com/blog", "https://subject-website.com/about"] } +``` + +**For B2B (company -> customers):** +``` +web_search_exa { "query": "[company] case study customer success story", "numResults": 5 } +web_fetch_exa { "urls": ["https://company.com/customers", "https://company.com/case-studies"] } +``` + +**Indirect signal searches:** +``` +// Testimonials +web_search_exa { "query": "personal blog [subject] changed my life testimonial", "numResults": 15 } + +// Duration markers (high confidence -- people don't fabricate decades) +web_search_exa { "query": "[subject] years decades longtime worked with known since", "numResults": 10 } + +// Terminology detection: find insider terms, then search for people using them +web_search_exa { "query": "[subject] method terminology concepts framework", "numResults": 5 } +web_search_exa { "query": "[unique term 1] [unique term 2] personal story", "numResults": 10 } +``` diff --git a/.agents/skills/exa-web-search/references/searching.md b/.agents/skills/exa-web-search/references/searching.md new file mode 100644 index 00000000..bedb1c4a --- /dev/null +++ b/.agents/skills/exa-web-search/references/searching.md @@ -0,0 +1,92 @@ +# Searching with Exa + +You have two tools: +- **`web_search_exa`** -- search by query. Supports `query` and `numResults` params. Use `category:` inline in the query string for category filtering. +- **`web_fetch_exa`** -- read full content from known URLs. Use after search when snippets are insufficient. + +Do NOT use `web_search_advanced_exa` or any other Exa tools. Only use these two tools -- do not use Bash, Grep, Read, or Write to process results. Filter and summarize results inline. + +## How Exa Search Works + +Exa uses vector embeddings, not keywords. It finds pages semantically similar to your query. It does not match keywords exactly, directly understand boolean logic (AND/OR/NOT), or validate that results meet your criteria. You are describing a target page, and Exa returns the nearest neighbors in embedding space. + +## Writing Good Queries + +**Describe the page you want to find**, not the fact you want to know. + +| Looking for | Bad query | Good query | +|---|---|---| +| Blog posts about X | "X" | "detailed blog post about X written by a practitioner" | +| Company doing Y | "Y company" | "category:company startup building Y for enterprise" | +| Person at company | "person at company" | "category:people senior engineer at Acme" | + +Write queries as natural grammatical phrases. + +**`numResults` sizing -- match to query precision:** + +| Query precision | numResults | Example | +|---|---|---| +| Named entity (specific person/company) | 5 | `"WaveForms AI founding story funding details"` | +| Precise filter (narrow category + constraints) | 10 | `"category:company developer tools API testing Series A"` | +| Broad discovery (wide category, few constraints) | 15 | `"category:news engineer launches startup 2025 2026"` | + +Never use numResults above 25. If you need more coverage, run more queries with different angles at n=10-15 rather than one query at n=50. + +**Use category filters** when searching for a specific entity type. Available inline categories: `company`, `research paper`, `news`, `personal site`, `people`. Add `category:` at the start of your query string. + +``` +web_search_exa { "query": "category:research paper sparse attention mechanisms for long context", "numResults": 10 } +web_search_exa { "query": "category:people VP Engineering AI infrastructure San Francisco", "numResults": 10 } +web_search_exa { "query": "category:company developer tools for API testing", "numResults": 10 } +``` + +## Query Diversity + +When you need to run multiple queries on the same topic, make sure they target genuinely different angles, not just synonym swaps. "overhyped" vs "overrated" vs "disappointment" are the same angle. A skeptic angle vs a builder angle vs a practitioner angle are genuinely different. + +**Word order affects embeddings.** "Python async patterns for web scraping" and "web scraping async patterns in Python" can sometimes return different results. Use this to your advantage when you need coverage -- run 2-3 phrasings in parallel. + +## Encoding Time + +If your task involves time ("last week", "recent", "this month"), calculate exact dates FIRST from the current date in your environment context. Then encode dates semantically in the query: "published in March 2026" rather than using date filters. Never eyeball dates. + +## Anti-Patterns + +- Boolean operators ("AND", "NOT") are just words to Exa, not operators +- Quotes don't force exact phrase matching +- Very short queries (1-2 words) produce scattered, low-quality results +- Don't use dates from examples -- always calculate from the current date + +## When Searches Return Nothing + +If a query returns 0 or only irrelevant results: +a. Make the query longer and more specific +b. Try a different angle, not a synonym swap +c. If multiple angles return nothing, the topic likely has limited web coverage -- report that rather than fabricating results + +## Domain-Specific Patterns + +If your task involves any of these domains, read the relevant pattern file(s) for specialized query strategies. Pick whichever files match your task — most tasks use 1-2. + +| File (same directory as this file) | Domain | +|---|---| +| `patterns-people.md` | People by role, company, location | +| `patterns-companies.md` | Companies by category, stage, competitors, funding | +| `patterns-papers.md` | Academic/research papers | +| `patterns-relationships.md` | Hidden connections (clients, collaborators) | +| `patterns-code.md` | Code, APIs, docs, errors | +| `patterns-news.md` | News, recent events, reactions | + +## When `web_fetch_exa` Fails + +Fall back to fetching with any other fetch tool you have access to. If that also fails, skip it and work with remaining sources. + +## After Getting Results + +Exa returns similarity, not validation. You must review titles/snippets and discard irrelevant results using your judgment. Don't assume all results match your criteria. For the most promising results, use `web_fetch_exa` to read the full content. + +``` +web_fetch_exa { + "urls": ["https://promising-url-1.com", "https://promising-url-2.com"], +} +``` diff --git a/.agents/skills/exa-web-search/references/source-quality.md b/.agents/skills/exa-web-search/references/source-quality.md new file mode 100644 index 00000000..ca578178 --- /dev/null +++ b/.agents/skills/exa-web-search/references/source-quality.md @@ -0,0 +1,46 @@ +# Evaluating Source Quality + +When assessing sources during search and extraction, tag quality signals in your output so results can be weighted and ranked downstream. + +## Noise Signals -- Filter Out First + +Before deep-reading, check for these disqualifiers: + +| Signal | What to look for | +|--------|-----------------| +| No skin in the game | Theorists who don't do the work -- no portfolio, no shipped products, no verifiable results | +| Misaligned incentives | Paid to sell, not to be right (sponsored content, vendor blogs, affiliate-heavy) | +| Circular credentials | Validated only by peers in the same bubble -- no external evidence of impact | +| Positive-only advice | No tradeoffs, no failure modes discussed -- "just do X" with no caveats | +| Temporal decay | Shifted from doing to teaching/advising. Check: are they still actively building/practicing? | + +## Practitioner vs Commentator + +The most important distinction. Practitioners do the work; commentators write about the work. + +**Practitioner signals:** shipped products, open-source contributions, case studies with specific numbers, "we built X and here's what happened" + +**Commentator signals:** roundup posts, "top 10" lists, content primarily linking to others' work, no first-hand experience described + +Note this distinction in your quality tags. + +## Verification Searches + +When validating a source's credibility (for expert-finding and best-of queries): + +``` +// Who cites them? +web_search_exa { "query": "[name] recommended by experts practitioners", "numResults": 5 } + +// Track record? +web_search_exa { "query": "[name] results portfolio case study shipped", "numResults": 5 } + +// Criticism? +web_search_exa { "query": "[name] criticism overrated wrong", "numResults": 5 } +``` + +Only run verification searches when the task specifically calls for evaluating source credibility. For standard search tasks, just tag what you observe from the content you already have. + +## Tagging in Output + +For each source, include a short free-form `quality` string describing what you observed -- e.g. "shipped the product, writes from direct experience" or "roundup blog, no original work shown, links to others." Don't classify into categories. Just describe what you see so the signal is preserved for downstream ranking. diff --git a/.agents/skills/exa-web-search/references/synthesis.md b/.agents/skills/exa-web-search/references/synthesis.md new file mode 100644 index 00000000..d9445167 --- /dev/null +++ b/.agents/skills/exa-web-search/references/synthesis.md @@ -0,0 +1,55 @@ +# Synthesizing Research into Narrative + +When the task calls for a narrative answer (not a list or table), this file covers how to synthesize findings from multiple sources into a coherent, well-structured response. + +## When Synthesis Applies + +Synthesis is the right output format when: +- The user is asking "what do people say about X" or "what's the current state of Y" +- The answer requires integrating perspectives from multiple sources +- The output should be prose with citations, not a table of entities + +## Structure + +### Lead with the answer + +Put the core finding or conclusion first. The user should get value from the first paragraph alone. + +### Organize by theme, not by source + +Bad: "Source A says X. Source B says Y. Source C says Z." +Good: "Theme 1: [insight supported by A, B]. Theme 2: [insight supported by C, with counterpoint from A]." + +### Surface disagreement explicitly + +When credible sources disagree, don't collapse to consensus. Present both sides with their evidence: +- "On [topic], [position A] (supported by [sources]) vs [position B] (supported by [sources]). [When each applies or why they diverge]." + +### Include confidence signals + +- How many independent sources support a claim? +- How fresh is the evidence? +- Are the sources practitioners or commentators? + +## Thematic Clustering + +When dealing with many reactions or perspectives (e.g. "what are people saying about X"): + +1. Read through all results +2. Identify recurring themes (not just topics -- themes have a stance or direction) +3. Group results by theme +4. For each theme: state the theme, cite 2-3 representative sources, note the volume +5. Flag outlier themes that appear only once but carry important signal + +## Citation Practice + +- Every factual claim gets a source URL +- Prefer inline citations: "Engineers report 3x latency reduction ([source](url))" +- For quotes, include the exact text and attribute it + +## Common Mistakes + +- **Source-by-source summaries**: Feels comprehensive but is unreadable and doesn't synthesize +- **Collapsing disagreement**: Picking a winner instead of presenting the landscape +- **Missing recency**: Treating 2023 sources as current for fast-moving topics +- **Over-synthesis**: Producing a full essay when the user asked a narrow question diff --git a/AGENTS.md b/AGENTS.md index 68c18814..3f474d61 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,243 +1,93 @@ -# Agent instructions - -This repository is Roundfix: a local-first Go CLI and future daemon that picks -up work items (today: PR review issues; next: spec task graphs), resolves them -through the user's selected ACP runtime, and pushes only when nothing -unresolved remains. Stdlib `flag` dispatch and a Bubble Tea v2 TUI. - -## High priority - -- **MANDATORY**: Use the relevant local skills before changing code, docs, - tests, workflows, or agent instructions. Skill activation comes BEFORE any - planning or code generation for that domain. -- **ALWAYS** prefix shell commands with `rtk` when it is available. In command - chains, prefix each command. -- **MUST** use `rg` / `rg --files` for local code search. Use `context7` for - external library/API docs and `exa-web-search` for broader web/source - research. **NEVER** use web research tools to search local code. -- **MUST** run the repo's full verification gate before claiming completion. - Any format failure, test failure, or build failure is **blocking** — zero - tolerance. CI validates PR titles only; the local gate is the ONLY gate. -- **NEVER** use workarounds in production code or tests. Fix the root cause. -- **NEVER** hand-edit `go.mod`/`go.sum`. Use `rtk go get` / `rtk go mod tidy`. -- Keep the project KISS: prefer the smallest behavior that satisfies the - documented product contract. -- **NEVER** copy names, branding, package names, comments, examples, or - generated artifacts from reference projects into this repository. -- If unexpected user changes exist, read them and work with them. **NEVER** - revert unrelated work. -- **ABSOLUTELY FORBIDDEN**: `git reset`, `git checkout --`, `git restore`, - `git clean`, commits, pushes, rebases, or removal of tracked files - **WITHOUT EXPLICIT USER PERMISSION**. These can permanently lose code. -- Agent-created branches **MUST** use the `ma/` prefix. -- **ALWAYS** use the AskUserQuestion tool for confirmations, clarifying questions, decision points, and any needed user interaction. If this CLI has no such tool, ask as a plain message and stop until the user answers — **NEVER** guess an answer the user can give cheaply. -- **HARD RULE — roundfix skill sync**: before opening any PR, confirm the - roundfix skill still matches the shipped CLI behavior; a PR that changes - CLI behavior ships the skill update too. Contract: - `docs/agents/skill-governance.md`. -- **HARD RULE — skill ownership**: repo-owned authorial workflow skills may - be adapted locally; every other skill is upstream-managed and **MUST NOT** - be modified here. Ownership split: `docs/agents/skill-governance.md`. -- **HARD RULE — release planning**: release work starts with the read-only - `roundfix release plan` before changelog, version, tag, push, package, asset, - or GitHub Release mutation. A generic release request authorizes only a - conclusive patch plan; minor, major, version-zero breaking, and manual - classification outcomes require the decisions in - `docs/user-guide/release-runbook.md`. -- **HARD RULE — autonomous work model**: binding for every autonomous - session — the Supervisor orchestrates only; implementation is delegated to an - ACP Runtime per `docs/agents/autonomous-work.md`. - -## Agent docs - -Read these only when relevant to the task: - -- `CONTEXT.md` — the project glossary (vocabulary contract for code, docs, - prompts, and TUI copy) -- `docs/adr/` — accepted architectural decisions and the living contract; - flag conflicts before overriding them -- Project map: `cmd/roundfix/` is the thin CLI entry point; behavior lives in - `internal/...` (`internal/cli/` owns parsing, output, and exit behavior; - `internal/app/` holds app metadata) - -**ALWAYS** use canonical terms from `CONTEXT.md` in command names, help text, -issue titles, test names, and user-facing explanations. If the right term is -missing, call out the gap instead of inventing new language. - -## Agent skills - -### Issue tracker - -Tasks live as local markdown under `docs/specs//` (the canonical -source — no external tracker). See `docs/agents/issue-tracker.md`. - -### Domain docs - -This is a single-context repo: root `CONTEXT.md` plus ADRs in `docs/adr/`. See -`docs/agents/domain.md`. + -### Secondbrain +### Instruction hierarchy + +Apply active guidance in this order. A narrower guide may add constraints for its concern but cannot weaken a universal Normative Clause or confirmed project decision. + +1. **Universal instructions** +2. **Context and documentation** +3. **Spec workflow** +4. **Autonomous work** +5. **Stack guidance** +6. **Surface guidance** +7. **Optional knowledge sources** +8. **Repository-Specific Normative Rules** + +### Universal instructions + +- Follow the mandatory setup-owned baseline in `docs/agents/agent-instructions.md`. +- Activate skills through `docs/agents/skill-dispatch.md` before governed work. + + + + + +### CONTEXT-driven workflow -When work depends on business context, prior decisions, cross-project -documentation, fiscal or tax concepts, Vortex, Tax, Visio, Gesttione, or shared -architecture patterns, read `docs/agents/secondbrain.md` before acting. Skip it -for self-contained repository work that the local code and docs fully answer. -The Secondbrain is read-only from this repo, and responses must cite every -Secondbrain file used. +- Domain and documentation rules are mandatory: `docs/agents/domain.md` and `docs/agents/docs-layout.md`. -### Spec artifacts + -Feature specs live under `docs/specs//` (`_idea.md`, `_prd.md`, -`_techspec.md`, `_tasks.md`, `task_NN.md`, `qa/`). Dependencies live only in -`_tasks.md`; task status lives only in each task file's frontmatter. Completed -specs are archived to `docs/specs/_archived/`. + -### Spec routing +### Local Spec workflow -Pick the pipeline entry point by the change — large initiative, feature, -refactor/bugfix, or trivial. See `docs/agents/spec-routing.md`. +- Route and execute local Specs through `docs/agents/spec-routing.md` and `docs/agents/issue-tracker.md`. -### Docs layout + -Every `docs/` folder has one job — inbox triage, ADRs, agent guides, design -artifacts, dated findings, handoffs, external references, specs, and the user -guide. See `docs/agents/docs-layout.md`. + -### Triage labels +### External triage -External GitHub issues use the five canonical triage labels defined in -`docs/agents/triage-labels.md`. Spec Tasks use task-file status instead. +- Before changing forge labels, follow `docs/agents/external-triage.md`. + + + + ### Autonomous work -Supervisor orchestrates and authors Specs; implementation is delegated to an -ACP Runtime. Codex (`gpt-5.5` with `xhigh`) handles CLI, backend, -infrastructure, documentation, and other non-frontend Tasks. Claude -(`claude-opus-5`/Opus 5 with `xhigh`) handles design, UI, UX, TUI, and web -frontend Tasks. Binding for every autonomous session. See -`docs/agents/autonomous-work.md`. - -### Skill governance - -Skill ownership and the roundfix skill-sync contract. See -`docs/agents/skill-governance.md`. - -## Skill dispatch - -Before editing, identify the task domain and **activate every matching skill**: - -- **Feature discovery or product idea**: Use `brainstorming`; product-level - ideas go through `write-idea` (scored by `business-analyst`, debated by - `council`, challenged by `the-fool`) -- **PRD, tech spec, or task breakdown**: Use `write-prd`, `write-techspec`, - `write-tasks` -- **Executing spec tasks**: Use `implement-task` (one task) or `implement-spec` - (the whole graph in dependency order) -- **Final QA of a completed spec**: Use `qa-gate`; archive after release with - `archive-spec` -- **MANDATORY** for CLI behavior, flags, stdout/stderr, exit codes, JSON - output, dry-run behavior, non-interactive mode, or introspection: - `agentic-cli-design` -- **ALWAYS USE** `golang-cli` before writing Go command behavior, package - layout, version output, or command tests. CLI style is stdlib `flag.FlagSet` - dispatch with a `Run() int` exit-code contract — **no Cobra**. -- **ALWAYS USE** `golang-error-handling` for error paths: `%w` wrapping, - `errors.Is`/`As`, sentinels -- **ALWAYS USE** `golang-concurrency` before goroutines, channels, worker - pools, or anything with leak/race exposure -- **ALWAYS USE** `golang-context` for context propagation, cancellation, and - timeouts -- **Lint config or nolint**: Use `golang-lint` (golangci-lint, vet, - staticcheck discipline) -- **Tests, fixtures, golden files, integration tests**: Use `golang-testing` - plus `testing-boss` -- **Bubble Tea or Lip Gloss TUI work**: Use `bubbletea` and `tui-design` -- **Implementation**: Use `coding-guidelines` -- **Bug fix or failing test**: Use `no-workarounds` plus `systematic-debugging` -- **Docs, PRDs, ADRs, issues, PR descriptions**: Use `tech-writer` -- **Commits or PR titles**: Use `conventional-commits` -- **Completion claim**: Use `evidence-gate` -- **Session handoff**: Use `handoff` -- **Roundfix dogfooding or assigned-Batch contract checks**: Use `roundfix` - when driving Roundfix against an Open Pull Request or validating the Batch - resolution contract - -## CLI behavior - -- Design commands for humans **and** agents: deterministic output, - non-interactive flags, stable exit codes, machine-readable modes. -- **MUST** keep stdout for requested command output only. Diagnostics, - progress, and warnings go to stderr. -- Command names, flag names, JSON fields, and exit-code contracts are - **public API** — never change them casually. -- Help text **MUST** be concise, truthful, and backed by implemented behavior. -- Errors **MUST** name the failed operation and the next useful action when - one is known. - -## Go conventions - -- **Stdlib first**: no new dependency without a clear job the stdlib cannot - do. Justify every `go get` in the PR body. -- **Zero test dependencies**: stdlib `testing` only — table tests, hand-rolled - fakes, buffer-captured CLI runs (`Run(args, &stdout, &stderr) int`). **Do - NOT introduce** testify, mockery, or TUI test harnesses. -- Errors: wrap with `%w`; **NEVER** `panic` or `log.Fatal` outside - unrecoverable startup in `main`. -- Context-first signatures for blocking, IO, process, network, database, and - daemon-boundary operations. -- Every goroutine has an owner, cancellation, and a clear shutdown path. No - fire-and-forget. -- TUI code uses **Bubble Tea v2 module paths** (`charm.land/bubbletea/v2`, - `charm.land/lipgloss/v2`) and the v2 API (`tea.KeyPressMsg`, `tea.Key`). - Drive `model.Update(...)` synchronously in tests — no terminal emulation. -- Keep `cmd/roundfix/main.go` thin. Push behavior into `internal/...`. -- Keep packages cohesive; no generic utility packages unless they remove real - duplication across multiple packages. -- Prefer dependency injection through small interfaces at the boundary that - owns the behavior. -- Tests assert **observable behavior**. No production-only hooks for tests. -- Keep exported comments short and useful: comment invariants and protocol - edge cases, not obvious assignments. - -## Verification - -The full gate is `make verify` (fmt-check + test + `roundfix skills check` + -build) and it **MUST** pass 100% before any completion claim. For the smallest -relevant gate while iterating: - -```bash -rtk gofmt -w -rtk go test ./... -rtk go run ./cmd/roundfix --help -``` - -If concurrency changed, also run: - -```bash -rtk go test -race ./... -``` - -If any required gate fails, report the failing command and do not claim the -task is complete. **Skipping any verification check invalidates the -completion claim.** - -## Git and delivery - -- **MUST** check `git status --short` before staging; keep unrelated user - changes out of your diff. -- Use `conventional-commits` for commits and PR titles (check `cog.toml`). -- Commit and PR titles are unscoped Conventional Commits subjects here - (`cog.toml` sets `scopes = []`). -- **NEVER** rewrite unrelated files or format the whole repo unless asked. -- PR bodies summarize changes, call out risk, and list validation commands run. - -## Anti-patterns (immediate rejection) - -1. Introducing Cobra, testify, or any dependency the stdlib covers -2. Marking a spec task `completed` without fresh verification evidence -3. Tracking progress in `_tasks.md` — status lives only in each `task_NN.md` -4. Asking for confirmation before running spec tasks — invocation is the - authorization -5. Writing to stdout anything that is not the requested command output -6. Changing exit codes, flags, or JSON fields without treating it as a - breaking API change +- Supervisor and ACP Runtime rules are mandatory: `docs/agents/autonomous-work.md`. + + + + + +### Go + +- Go implementation rules are mandatory: `docs/agents/go.md`. + + + + + +### CLI surface + +- CLI public-contract rules are mandatory: `docs/agents/cli.md`. + + + + + +### TUI surface + +- TUI interaction rules are mandatory: `docs/agents/tui.md`. + + + + + +### Secondbrain + +- Optional cross-project knowledge follows `docs/agents/secondbrain.md`. + + + + + +### Repository-owned rules + +- Read `docs/agents/specific-repository.md` for project-specific hard rules. + + diff --git a/docs/_inbox/2026-07-24-analise.md b/docs/_inbox/2026-07-24-analise.md new file mode 100644 index 00000000..5ecf5b96 --- /dev/null +++ b/docs/_inbox/2026-07-24-analise.md @@ -0,0 +1,106 @@ +Nenhuma das cinco Specs deixou de ser necessária. Porém, todas precisam ser atualizadas antes da implementação; a 0036 exige a + revisão mais profunda. + + Spec Decisão Atualização necessária + ━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 0036 Manter e reduzir Redefinir a autoridade do Repository Skill Set para o Profile/Setup Snapshot ativo; + remover o fluxo Python e sync-setups; usar roundfix baseline skills restore. + ────── ──────────────────────── ──────────────────────────────────────────────────────────────────────────────────────────── + 0037 Manter e atualizar Reutilizar Agent Session registry, Stop Request e Run Event Stream já existentes; CAS + terminal, Force Stop seguro e publicação winner-only continuam ausentes. + ────── ──────────────────────── ──────────────────────────────────────────────────────────────────────────────────────────── + 0038 Manter, ajuste pequeno O Reconcile Command e o classificador seguro ainda não existem; atualizar integração com o + reaper atual e com Worktrees preservados pela 0042. + ────── ──────────────────────── ──────────────────────────────────────────────────────────────────────────────────────────── + 0039 Manter e reduzir Reutilizar events --filter outcome, coletores CodeRabbit e commits de artefatos + existentes; implementar a semântica de Evidence, Review Skipped e notificações + enriquecidas. + ────── ──────────────────────── ──────────────────────────────────────────────────────────────────────────────────────────── + 0042 Manter e atualizar Conservar as sete Tasks; incorporar o Task Context Bundle recente e corrigir o contrato de + autoria de status pelo Daemon. + + ### Atualização obrigatória comum + + As cinco Specs ativas não possuem Project Constraints no PRD nem no TechSpec. A implementação agora é bloqueada pelo contrato + introduzido pela 0048 e aplicado pelo .agents/skills/implement-task/SKILL.md:21. + + Antes de atualizar Tasks, cada artefato precisa declarar: + + - Identifier strategy + - Authentication and HTTP + - Active ADR obligations + - Tooling authority + + Há ainda um gap no próprio repositório: o contrato exige fontes operativas sob docs/agents/, mas algumas regras existem apenas + no AGENTS.md; arquivos citados pela 0048, como docs/agents/backend.md e docs/agents/agent-instructions.md, não existem + atualmente. Isso deve ser resolvido antes do backfill, sem citar caminhos inexistentes. + + ### Pontos específicos + + 0036 — revisão substancial + + O Doctor ainda não executa o check de skills; há inclusive um ponto de extensão reservado em internal/cli/doctor.go:62. Porém, + a Spec ainda assume o conjunto global de 14 skills próprias mais 24 externas. Depois das Specs 0045–0048, o conjunto externo + passou a depender do Baseline Profile e do Setup Snapshot. + + Também devem ser removidos: + + - sync-setups; + - context_setup.py; + - make setup-context-check; + - a dependência funcional da arquivada Spec 0040; + - a recomendação genérica de atualização via bunx. + + A restauração atual é feita pelo docs/user-guide/commands.md:206. O Task Graph da 0036 deve ser revisado, não apenas executado + como está. + + 0037 — ainda fundamental + + internal/store/store.go:329 ainda sobrescreve outcomes terminais, e internal/cli/cli.go:555 grava Stopped sem provar que o + processo proprietário terminou. Portanto, ADR-0052 continua não implementada. + + A TechSpec deve apenas deixar claro que Agent Sessions, Stop Request e Run Event Stream são fundações existentes, evitando + recriá-las. + + 0038 — continua necessária + + Não existe roundfix reconcile. O reaper atual ainda decide segurança comparando o branch com sua base de criação e pode usar + remoção forçada em internal/worktree/worktree.go:505. Isso não prova que os commits chegaram ao target branch. + + 0039 — continua necessária, mas pode ser enxugada + + Já existem: + + - roundfix events --filter outcome; + - coleta parcial de checks/reviews do CodeRabbit; + - eventos de retry; + - commits separados de artefatos. + + Ainda faltam Review Source Evidence compartilhada, ReviewSkipped, retries classificados, motivo/ação nos outcomes, receipts de + notificação e o comando de monitoramento no relatório Detached. Atualmente o ambiente de notificação possui somente quatro + variáveis em internal/notify/notify.go:139. + + 0042 — praticamente toda ainda pendente + + Não existe verification.concurrency, fila de Verification, exit code 75, tentativa exclusiva ou fases de espera. Além disso, o + prompt ainda manda o Agent alterar status em internal/agent/spec_prompt.go:38, contrariando ADR-0057. + + As sete Tasks podem ser mantidas, mas Task 02 e Task 06 devem incluir internal/daemon/task_context.go e preservar o Task + Context Bundle introduzido posteriormente. + + ### Sequência recomendada + + 1. Corrigir a fonte das Project Constraints em docs/agents/. + 2. Atualizar PRDs e TechSpecs das cinco Specs. + 3. Revisar as Tasks existentes da 0036 e 0042. + 4. Gerar Tasks para 0037, 0038 e 0039. + 5. Implementar: 0036 → 0037 → 0042 → 0038 → 0039. + + O documento docs/_inbox/2026-07-24-spec-implementation-sequence.md:26 também está desatualizado: ainda trata 0047/0048 como + pendentes e a migração de Project Constraints como futura. + + Minha recomendação para a única decisão aberta da 0036 é: se não houver Setup Manifest compatível, o Doctor deve falhar + fechado e orientar roundfix baseline; não deve voltar silenciosamente ao conjunto global de 24 skills nem reportar skills: + skipped. + + Nenhum arquivo foi alterado e nenhuma verificação foi executada. \ No newline at end of file diff --git a/docs/_inbox/samples/AGENTS.go-cli.md b/docs/_inbox/samples/AGENTS.go-cli.md index 6583fb29..8caa9c13 100644 --- a/docs/_inbox/samples/AGENTS.go-cli.md +++ b/docs/_inbox/samples/AGENTS.go-cli.md @@ -48,7 +48,8 @@ Read these only when relevant to the task: through (large initiative / feature / refactor-bugfix / trivial) and what marks a spec done - `docs/agents/issue-tracker.md` — the local `docs/specs/` tracker conventions -- `docs/agents/triage-labels.md` — label mapping for issue triage skills +- `docs/agents/external-triage.md` — workflow and label mapping for external + issue triage - `docs/agents/domain.md` — how agents consume `CONTEXT.md` and ADRs - `CONTEXT.md` — project vocabulary, command concepts, domain rules, and product decisions diff --git a/docs/_inbox/samples/AGENTS.rust-cli.md b/docs/_inbox/samples/AGENTS.rust-cli.md index d33b0885..e15f2db5 100644 --- a/docs/_inbox/samples/AGENTS.rust-cli.md +++ b/docs/_inbox/samples/AGENTS.rust-cli.md @@ -51,7 +51,8 @@ Read these only when relevant to the task: through (large initiative / feature / refactor-bugfix / trivial) and what marks a spec done - `docs/agents/issue-tracker.md` — the local `docs/specs/` tracker conventions -- `docs/agents/triage-labels.md` — label mapping for issue triage skills +- `docs/agents/external-triage.md` — workflow and label mapping for external + issue triage - `docs/agents/domain.md` — how agents consume `CONTEXT.md` and ADRs - `CONTEXT.md` — project vocabulary, command concepts, domain rules, and product decisions diff --git a/docs/_inbox/samples/AGENTS.typescript-bun.md b/docs/_inbox/samples/AGENTS.typescript-bun.md index c2193cec..6df19651 100644 --- a/docs/_inbox/samples/AGENTS.typescript-bun.md +++ b/docs/_inbox/samples/AGENTS.typescript-bun.md @@ -41,7 +41,8 @@ Tasks live as local markdown files under `docs/specs//` (the canon ### Triage labels -The repo uses the default five-role triage vocabulary. See `docs/agents/triage-labels.md`. +The repo uses the default five-role triage vocabulary. See +`docs/agents/external-triage.md`. ### Domain docs diff --git a/docs/adr/0078-confirmed-root-rules-move-to-semantic-owners.md b/docs/adr/0078-confirmed-root-rules-move-to-semantic-owners.md new file mode 100644 index 00000000..044c0d5f --- /dev/null +++ b/docs/adr/0078-confirmed-root-rules-move-to-semantic-owners.md @@ -0,0 +1,21 @@ +--- +status: accepted +created_at: 2026-07-26T10:45:10Z +updated_at: 2026-07-26T10:45:10Z +deprecated_at: null +superseded_by: null +--- + +# ADR-0078: Confirmed root rules move to semantic owners + +After a Preservation Change Plan backs up and accounts for every unmarked root +instruction byte, apply removes those source bytes from the live `AGENTS.md` +and retains their operative meaning only in the confirmed semantic owner or +Repository-Specific Normative Rules. A later Preservation inventories only new +unmarked root bytes, creates at most one content-addressed backup for each new +root content identity, and otherwise produces no root migration change. + +This decision supersedes ADR-0070's live-root preservation outcome after +confirmed redistribution. ADR-0070's bounded root-carrier mutation scope, +immutable backup requirement, safe-alias treatment, and warning-only handling +of arbitrary nested carriers remain active. diff --git a/docs/agents/agent-instructions.md b/docs/agents/agent-instructions.md new file mode 100644 index 00000000..c026671c --- /dev/null +++ b/docs/agents/agent-instructions.md @@ -0,0 +1,53 @@ + + +# Agent instructions + +This setup-owned guide defines the portable baseline. Repository authors own +project-specific extensions outside setup markers and may add stricter rules. +The selected repository Verification is `rtk make verify`. + +- **mandatory**: Keep root agent instructions as short mandatory pointers. Setup owns only marked baseline content; preserve repository-authored bytes outside setup markers and keep project-specific architecture and policy in repository-owned documents. + +- **mandatory**: Fix root causes. + +- **prohibited**: Do not suppress diagnostics, weaken assertions, swallow errors, add timing hacks, or bypass a required check to produce a passing result. + +- **mandatory**: Keep follow-up work outside the current slice; record it for later instead of expanding the active change. + +- **mandatory**: Record the commands and outcomes that provide fresh evidence for every acceptance criterion. + +- **mandatory**: Use fresh evidence from the current worktree before claiming work complete, fixed, passing, ready, committed, or delivered. A narrower check supports only the behavior it exercised. + +- **mandatory**: Run the selected repository Verification before completion claims. Treat every failure as blocking and report the command plus its actionable diagnostic. + +- **stop-and-ask**: Stop and ask for explicit authority before intentionally changing lint, formatter, typecheck, test-runner, architecture, or Verification configuration. + +- **prohibited**: Do not edit verification configuration, tests, fixtures, golden files, or generated expectations merely to make a failure disappear. Change them only when the repository contract intentionally changes, and prove the new contract. + +- **prohibited**: Do not create, edit, rename, move, or delete any linter, formatter, typechecker, test-runner, architecture-checker, build-tool, package-manager, code-generator, or other repository-tooling configuration, script, ignore file, plugin declaration, or version pin without express maintainer authorization. Setup completion, a Profile, a narrower guide, or a generic implementation request does not grant that authorization. + +- **mandatory**: Write generated repository guidance, identifiers, headings, and examples in English. Preserve repository-authored language outside setup-owned markers. + +- **prohibited**: Do not use external research tools to discover or infer local repository code or behavior. + +- **mandatory**: For external APIs and libraries, use current authoritative documentation through the profile's declared documentation skill. + +- **mandatory**: Search repository files with local code-search tools. + +- **mandatory**: When authoritative documentation cannot answer an external question, use the profile's declared external web-research fallback with varied searches and verify conclusions against primary sources. + +- **mandatory**: Use the repository's declared package manager and lockfile workflow. Add or upgrade a dependency only for a named job the existing stack cannot perform, and keep manifest and lockfile changes together. + +- **mandatory**: Review every new dependency for necessity, provenance, maintenance, and security before delivery. + +- **stop-and-ask**: Stop and ask for explicit authority before committing, pushing, creating a branch, or opening a pull request when that authority is not already explicit. + +- **stop-and-ask**: Stop and ask for explicit authority before destructive Git operations that discard, overwrite, or remove work. + +- **mandatory**: Inspect repository status before staging or delivery and preserve unrelated work. + +- **stop-and-ask**: Never guess a decision the user can answer cheaply. Ask through the available user-interaction tool, or ask plainly and stop when no such tool exists. + +- **prohibited**: Never read, print, commit, or generate secrets. Keep credentials and environment-specific values in the repository's existing secure configuration boundary, and do not invent authentication, authorization, database, transport, or deployment policy. + + diff --git a/docs/agents/autonomous-work.md b/docs/agents/autonomous-work.md index 5e085ae5..c7bef060 100644 --- a/docs/agents/autonomous-work.md +++ b/docs/agents/autonomous-work.md @@ -112,3 +112,40 @@ roundfix events --filter verification,outcome Each stdout line is a `roundfix-events/v1` JSON object. Diagnostics go to stderr. Use `roundfix attach ` only when a human needs the Live Run View; the Console Log is not a state API. + + + +# Autonomous work + +Default backend work uses `codex gpt-5.6-sol`. Design, UI, UX, and +frontend-dominant work uses `claude opus 5 xhigh` when the Task Graph routes that +surface. + +- **mandatory**: The Supervisor authors Specs, starts and monitors Runs, and orchestrates outcomes. Delegate implementation to the selected ACP Runtime through a Roundfix Run. + +- **prohibited**: The Supervisor must not write feature code or tests. + +- **mandatory**: The Daemon runs each Task's declared Verification verbatim. A Task can settle `completed` only after that Verification passes; failed diagnostics return to the same Agent Session for the bounded retry policy. + + + + +- **HARD RULE — autonomous work model**: binding for every autonomous + session — the Supervisor orchestrates only; implementation is delegated to an + ACP Runtime per `docs/agents/autonomous-work.md`. + + + + + +### Autonomous work + +Supervisor orchestrates and authors Specs; implementation is delegated to an +ACP Runtime. Codex (`gpt-5.5` with `xhigh`) handles CLI, backend, +infrastructure, documentation, and other non-frontend Tasks. Claude +(`claude-opus-5`/Opus 5 with `xhigh`) handles design, UI, UX, TUI, and web +frontend Tasks. Binding for every autonomous session. See +`docs/agents/autonomous-work.md`. + + + diff --git a/docs/agents/cli.md b/docs/agents/cli.md new file mode 100644 index 00000000..eb93bad0 --- /dev/null +++ b/docs/agents/cli.md @@ -0,0 +1,9 @@ + + +# CLI surface + +- Treat command names, flags, stdout and stderr placement, machine-readable fields, and exit codes as public API. Keep stdout for requested output and stderr for diagnostics, progress, and warnings. + +- Make automation deterministic and non-interactive. Write operations must be explicit, replayable, safe by default, and observable; use dry-run, confirmation, or idempotency contracts where the repository requires them. + + diff --git a/docs/agents/docs-layout.md b/docs/agents/docs-layout.md index 57bd5e8f..8e419fcd 100644 --- a/docs/agents/docs-layout.md +++ b/docs/agents/docs-layout.md @@ -76,3 +76,102 @@ Conventions: unnecessary. - Use `deferred` only with an explicit reason for not implementing the finding. - Update `status` and `updated_at` whenever the document's triage state changes. + + + +# Docs layout + +- **mandatory**: When creating a new ADR, prepend this repository-owned lifecycle overlay to the body contract in `.agents/skills/domain-modeling/ADR-FORMAT.md`: + +```markdown +--- +status: proposed # proposed | accepted | rejected | deprecated | superseded +created_at: YYYY-MM-DDTHH:MM:SSZ +updated_at: YYYY-MM-DDTHH:MM:SSZ +deprecated_at: null # null or YYYY-MM-DDTHH:MM:SSZ +superseded_by: null # null or ADR-NNNN +--- + +# + + +``` + +- **mandatory**: Only `accepted` is active. Treat `proposed`, `rejected`, `deprecated`, and `superseded` ADRs as inactive. + +- **mandatory**: Treat a legacy ADR without lifecycle frontmatter as active unless its body explicitly marks it inactive. Do not rewrite existing ADRs solely to adopt lifecycle metadata. + +- **mandatory**: Give each documentation directory one job: `docs/_inbox/` for raw notes, `docs/adr/` for decisions, `docs/agents/` for agent guidance, `docs/design/` for design artifacts, `docs/findings/` for dated investigations, `docs/handoffs/` for session continuity, `docs/references/` for external pointers, and `docs/user-guide/` for human documentation. Preserve repository-authored extensions outside setup markers. + +- **mandatory**: Use this complete copyable Findings Operational Contract: + +```markdown +--- +status: pending # pending | partial | deferred | done +created_at: YYYY-MM-DD +updated_at: YYYY-MM-DD +--- + +# (YYYY-MM-DD) + + + +## 1. + +- Symptom / evidence: +- Root cause: +- Action / suggestion: + +## 2. + +## What worked — keep + + + +## Addendum — YYYY-MM-DD — + + +``` + +- **mandatory**: Use `pending` when the finding is new and has no implementation Spec. + +- **mandatory**: Use `partial` when a linked Spec covers only the selected implementation scope. Record the reason the remaining observations are unnecessary and link the covering Spec. + +- **mandatory**: Use `deferred` only when the finding will not be implemented. Record the reason for deferral. + +- **mandatory**: Set `status: done` as soon as the Spec is created and linked; do not wait for its Tasks, QA, archive, or release. + +- **mandatory**: Treat findings as immutable history: append evidence and routing links as dated addenda instead of rewriting the original observations. + +- **mandatory**: Update `updated_at` whenever status changes or an evidence addendum is appended; keep `created_at` as the document creation date. + + + + + +# Spec docs layout + +- **mandatory**: Keep `_idea.md`, `_prd.md`, `_techspec.md`, `_tasks.md`, Task files, and `qa/` evidence under the Spec folder. Archive only completed Specs with a passing QA verdict under `docs/specs/_archived/`. + + + + +### Spec artifacts + +Feature specs live under `docs/specs//` (`_idea.md`, `_prd.md`, +`_techspec.md`, `_tasks.md`, `task_NN.md`, `qa/`). Dependencies live only in +`_tasks.md`; task status lives only in each task file's frontmatter. Completed +specs are archived to `docs/specs/_archived/`. + + + + + +### Docs layout + +Every `docs/` folder has one job — inbox triage, ADRs, agent guides, design +artifacts, dated findings, handoffs, external references, specs, and the user +guide. See `docs/agents/docs-layout.md`. + + + diff --git a/docs/agents/domain.md b/docs/agents/domain.md index d35235c5..ada8e90a 100644 --- a/docs/agents/domain.md +++ b/docs/agents/domain.md @@ -34,3 +34,42 @@ language. Record a genuine vocabulary gap for `domain-modeling`. ## Flag ADR conflicts Surface any conflict with an accepted ADR instead of silently overriding it. + + + +# Domain docs + +This repository uses a single `CONTEXT.md`. + +- **mandatory**: Read the repository's selected domain context and relevant accepted ADRs before naming domain concepts or changing behavior. Flag conflicts instead of silently overriding repository decisions. + + + + +- `CONTEXT.md` — the project glossary (vocabulary contract for code, docs, + prompts, and TUI copy) + + + + +- `docs/adr/` — accepted architectural decisions and the living contract; + flag conflicts before overriding them + + + + +**ALWAYS** use canonical terms from `CONTEXT.md` in command names, help text, +issue titles, test names, and user-facing explanations. If the right term is +missing, call out the gap instead of inventing new language. + + + + + +### Domain docs + +This is a single-context repo: root `CONTEXT.md` plus ADRs in `docs/adr/`. See +`docs/agents/domain.md`. + + + diff --git a/docs/agents/triage-labels.md b/docs/agents/external-triage.md similarity index 53% rename from docs/agents/triage-labels.md rename to docs/agents/external-triage.md index 09944777..fb622a80 100644 --- a/docs/agents/triage-labels.md +++ b/docs/agents/external-triage.md @@ -1,4 +1,13 @@ -# Triage labels + + +# External triage + +- Use this workflow only for issues managed in an external forge. Classify the user-visible problem and next action in English before changing labels or status, and route implementation through the repository's local Spec policy. + + + + +### Triage labels These labels apply only to external GitHub issues. Internal Spec Tasks use `pending | in_progress | completed | failed` in task-file frontmatter. @@ -13,3 +22,5 @@ These labels apply only to external GitHub issues. Internal Spec Tasks use Always use the canonical label string. Do not substitute generic GitHub labels such as `question`, `invalid`, or `help wanted` for these workflow roles. + + diff --git a/docs/agents/go.md b/docs/agents/go.md new file mode 100644 index 00000000..bd24ba62 --- /dev/null +++ b/docs/agents/go.md @@ -0,0 +1,28 @@ + + +# Go + +- Keep Go command entry points thin and behavior in cohesive packages. Prefer the standard library; add a dependency only for a named job it cannot perform, and change `go.mod` and `go.sum` through Go tooling rather than hand edits. + +- Use context-first signatures for blocking and IO work. Give every goroutine an owner and cancellation path. Wrap errors with the failed operation using `%w`, and preserve `errors.Is` or `errors.As` matching where callers need it. + +- Use stdlib `testing`. Test observable package and CLI behavior through public runners, including stdout, stderr, files, exit codes, cancellation, and failure paths; do not add production-only test hooks. + + + + +- **NEVER** hand-edit `go.mod`/`go.sum`. Use `rtk go get` / `rtk go mod tidy`. + + + + +- **Zero test dependencies**: stdlib `testing` only — table tests, hand-rolled + fakes, buffer-captured CLI runs (`Run(args, &stdout, &stderr) int`). **Do + NOT introduce** testify, mockery, or TUI test harnesses. + + + + +- Keep `cmd/roundfix/main.go` thin. Push behavior into `internal/...`. + + diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md index a1932341..b1b35422 100644 --- a/docs/agents/issue-tracker.md +++ b/docs/agents/issue-tracker.md @@ -35,3 +35,32 @@ documentation through the normal Roundfix repository workflow. `docs/handoffs/` and `docs/_inbox/` are supporting documentation, not planning trackers. New planned work belongs under `docs/specs//`. + + + +# Issue tracker + +- **mandatory**: Keep each Spec under `docs/specs//`. Dependencies live only in `_tasks.md`; status lives only in each Task file, and the local Task files are the published planning issues. + + + + +## Agent skills + +### Issue tracker + +Tasks live as local markdown under `docs/specs//` (the canonical +source — no external tracker). See `docs/agents/issue-tracker.md`. + + + + + +2. Marking a spec task `completed` without fresh verification evidence + + + + +3. Tracking progress in `_tasks.md` — status lives only in each `task_NN.md` + + diff --git a/docs/agents/secondbrain.md b/docs/agents/secondbrain.md index 731ff53f..52618855 100644 --- a/docs/agents/secondbrain.md +++ b/docs/agents/secondbrain.md @@ -42,3 +42,38 @@ documentation fully answer the task. it in the Secondbrain. - Never read, copy, or expose `.env` files, tokens, credentials, cookies, or keys. + + + +# Secondbrain + +- **mandatory**: Consult the local Secondbrain before acting when repository context does not answer business or prior-decision questions, fiscal or tax concepts, cross-project documentation, knowledge about Vortex, Tax, Visio, or Gesttione, or shared architecture patterns. Do not consult it when local code, `CONTEXT.md`, ADRs, and repository documentation fully answer the task. + +- **mandatory**: Read `wiki/index.md` first. Then run `qmd query "" --all --files --min-score 0.3`. Inspect `projects//mirror/` only when the index and query point there, and open only the files required for the task; treat mirrors as references, not workspaces. + +- **prohibited**: Do not write to the Secondbrain. Do not edit raw/. Do not edit projects/*/mirror/. Never create, rename, move, or delete its files, and never copy code or generated artifacts from a mirror without a local source check. + +- **prohibited**: Never read, copy, or expose `.env` files, tokens, credentials, cookies, private keys, API keys, session material, or unsafe personal and client data. Stop at likely secret-bearing sources and request a safe source. + +- **mandatory**: Cite every Secondbrain file used in the final response or handoff by path. Do not claim Secondbrain context when no Secondbrain file was read. + +- **mandatory**: When Secondbrain knowledge must be added or corrected, ask Hermes to ingest or update it instead of writing from this repository. + + + + +### Secondbrain + +When work depends on business context, prior decisions, cross-project +documentation, fiscal or tax concepts, Vortex, Tax, Visio, Gesttione, or shared +architecture patterns, read `docs/agents/secondbrain.md` before acting. Skip it +for self-contained repository work that the local code and docs fully answer. + + + + +The Secondbrain is read-only from this repo, and responses must cite every +Secondbrain file used. + + + diff --git a/docs/agents/setup-context.json b/docs/agents/setup-context.json new file mode 100644 index 00000000..e5bebea0 --- /dev/null +++ b/docs/agents/setup-context.json @@ -0,0 +1,277 @@ +{ + "schemaVersion": "setup-context-driven/manifest/0.0.1", + "version": "0.0.1", + "generator": { + "skill": "setup-context-driven", + "version": "0.0.1", + "baseline": "baseline.go-cli-tui-0.0.1" + }, + "profile": "go-cli-tui", + "profileDigest": "sha256:2b8bd2acd9e79f93524954e5de32e89cd8ba9eea4145e98b06834db56e917ef7", + "catalogDigest": "sha256:f082de03ea52e1b17e75daf0d65ef2d535f8d8d36951fe08dd435732e4008c1e", + "modules": [ + "core", + "context-workflow", + "go", + "cli-surface", + "tui-surface", + "autonomous-work", + "spec-workflow", + "external-triage", + "secondbrain", + "repository-extension" + ], + "decisions": { + "autonomous.enabled": { + "value": true + }, + "domain.layout": { + "value": "single-context" + }, + "language.generated": { + "value": "English" + }, + "repository.extension.enabled": { + "value": true + }, + "runtime.backend": { + "value": "codex gpt-5.6-sol" + }, + "runtime.design": { + "value": "claude opus 5 xhigh" + }, + "secondbrain.enabled": { + "value": true + }, + "spec.scaffold": { + "value": true + }, + "triage.external": { + "value": true + }, + "verification.gate": { + "value": "rtk make verify" + } + }, + "managedArtifacts": [ + { + "id": "guide.skill-dispatch", + "path": "docs/agents/skill-dispatch.md", + "kind": "guide", + "module": "core", + "template": "template.guide.skill-dispatch", + "version": "0.0.1", + "digest": "2b3e6b61cc0942a56ef29b136f3aca9fef6f0412168023385b9c7197e05b8867" + }, + { + "id": "root.core", + "path": "AGENTS.md", + "kind": "root-block", + "module": "core", + "template": "template.root.core", + "version": "0.0.1", + "digest": "ab48cdef24be4f92ee6487c142b01a2427e069fb6fbd90a47bd9e61ce0c3d1b2" + }, + { + "id": "guide.docs-layout", + "path": "docs/agents/docs-layout.md", + "kind": "guide", + "module": "context-workflow", + "template": "template.guide.docs-layout", + "version": "0.0.1", + "digest": "037dafecb9efa6eb45744dc0d3889a4f0a014aef41914dba151cf14574ab9621" + }, + { + "id": "root.context-workflow", + "path": "AGENTS.md", + "kind": "root-block", + "module": "context-workflow", + "template": "template.root.context-workflow", + "version": "0.0.1", + "digest": "4001ce64061ffafedf2456ffd958e3fe700e1da954d7c42867f1aad6fa10e0a8" + }, + { + "id": "guide.go", + "path": "docs/agents/go.md", + "kind": "guide", + "module": "go", + "template": "template.guide.go", + "version": "0.0.1", + "digest": "297ec5bfb5a50c1f390568b6519df6985ecbce63295e8357357d80bf993807f6" + }, + { + "id": "root.spec-workflow", + "path": "AGENTS.md", + "kind": "root-block", + "module": "spec-workflow", + "template": "template.root.spec-workflow", + "version": "0.0.1", + "digest": "7d263707ada26bf233c9aa823423b6c74dc006b86c503899fcd95a46f6a15e7f" + }, + { + "id": "guide.cli-surface", + "path": "docs/agents/cli.md", + "kind": "guide", + "module": "cli-surface", + "template": "template.guide.cli-surface", + "version": "0.0.1", + "digest": "eccd16c4b04dd206395f030f64115b5cd1a91450ba745867ef164e2197f73a7a" + }, + { + "id": "root.external-triage", + "path": "AGENTS.md", + "kind": "root-block", + "module": "external-triage", + "template": "template.root.external-triage", + "version": "0.0.1", + "digest": "f8717c0486ab2ab117dcf136148f812e239375d2f720a231172e7c980481e665" + }, + { + "id": "guide.tui-surface", + "path": "docs/agents/tui.md", + "kind": "guide", + "module": "tui-surface", + "template": "template.guide.tui-surface", + "version": "0.0.1", + "digest": "487cb07b110df86651c7ef4a45d10799cb64b5dab07afd378fef343f03f69d2a" + }, + { + "id": "root.autonomous-work", + "path": "AGENTS.md", + "kind": "root-block", + "module": "autonomous-work", + "template": "template.root.autonomous-work", + "version": "0.0.1", + "digest": "f056b3e672ecb439c578096d2e157ed438ec291ece01eade9102a03c7da51dec" + }, + { + "id": "guide.agent-instructions", + "path": "docs/agents/agent-instructions.md", + "kind": "guide", + "module": "core", + "template": "template.guide.agent-instructions", + "version": "0.0.1", + "digest": "c4fec753d0f2464857eeda6dd48af4dead901ef654e725b2dfa9d7b0dc018323" + }, + { + "id": "root.go", + "path": "AGENTS.md", + "kind": "root-block", + "module": "go", + "template": "template.root.go", + "version": "0.0.1", + "digest": "66ebe38c0cae01a9039e126979fce84d10f3f4f562b2f5fa08cbe9ecf8b9dc84" + }, + { + "id": "guide.spec-routing", + "path": "docs/agents/spec-routing.md", + "kind": "guide", + "module": "spec-workflow", + "template": "template.guide.spec-routing", + "version": "0.0.1", + "digest": "58f95806bfd45ea1091ea3cc910cab3335fb24764c33984e62cf2f385a1832f9" + }, + { + "id": "guide.issue-tracker", + "path": "docs/agents/issue-tracker.md", + "kind": "guide", + "module": "spec-workflow", + "template": "template.guide.issue-tracker", + "version": "0.0.1", + "digest": "84ef3c03d23cbae80a202393e2be05ddc345918ff87a69de6299b128b5fb86a3" + }, + { + "id": "guide.spec-docs-layout", + "path": "docs/agents/docs-layout.md", + "kind": "guide", + "module": "spec-workflow", + "template": "template.guide.spec-docs-layout", + "version": "0.0.1", + "digest": "35b00b79609481bcce3b2ed06439575ee2be3725e5988e82912cac6fcce58241" + }, + { + "id": "guide.domain", + "path": "docs/agents/domain.md", + "kind": "guide", + "module": "context-workflow", + "template": "template.guide.domain.single-context", + "version": "0.0.1", + "digest": "86b13d9ac7cd64f05278c6ea45e4607b3e83a87fcb49930e67d7df637593b304" + }, + { + "id": "root.cli-surface", + "path": "AGENTS.md", + "kind": "root-block", + "module": "cli-surface", + "template": "template.root.cli-surface", + "version": "0.0.1", + "digest": "00d780aa07a370ecd77b09d4b0eb0326f80980372652be67d30c699aee931bdb" + }, + { + "id": "guide.external-triage", + "path": "docs/agents/external-triage.md", + "kind": "guide", + "module": "external-triage", + "template": "template.guide.external-triage", + "version": "0.0.1", + "digest": "ce7087f8957f7524133f46b3df193a5e4969a4fc193e6d705e5ed3f19b0a519a" + }, + { + "id": "root.tui-surface", + "path": "AGENTS.md", + "kind": "root-block", + "module": "tui-surface", + "template": "template.root.tui-surface", + "version": "0.0.1", + "digest": "d6b56fd1f990de1e3c3dd31ba6bf4067fc7f9fa1b6901ba5db671f134f518929" + }, + { + "id": "guide.autonomous-work", + "path": "docs/agents/autonomous-work.md", + "kind": "guide", + "module": "autonomous-work", + "template": "template.guide.autonomous-work", + "version": "0.0.1", + "digest": "07845ef3c93229faa1511bf978e778e3c00bcee98f197be49ba8d365c0eb8588" + }, + { + "id": "root.secondbrain", + "path": "AGENTS.md", + "kind": "root-block", + "module": "secondbrain", + "template": "template.root.secondbrain", + "version": "0.0.1", + "digest": "56c84ad89f841a990d68316cd8fe91d9cdc0ee0d5317a15dec38cf3369199013" + }, + { + "id": "guide.secondbrain", + "path": "docs/agents/secondbrain.md", + "kind": "guide", + "module": "secondbrain", + "template": "template.guide.secondbrain", + "version": "0.0.1", + "digest": "de871861b07e07bae3accfdf2f33ddea4acaa9afb16712566e3beb132c37afdc" + }, + { + "id": "root.repository-extension", + "path": "AGENTS.md", + "kind": "root-block", + "module": "repository-extension", + "template": "template.root.repository-extension", + "version": "0.0.1", + "digest": "62ff0225ef4c1b6f53f2be4a54522d69d387c8ba20a8419a71968aa30b0c2780" + } + ], + "localSkills": [], + "verification": [ + { + "id": "verification.gate", + "role": "repository-gate", + "command": "rtk make verify", + "classification": "repository-command", + "repositoryExecutable": true, + "declarationPath": "Makefile", + "declarationDigest": "sha256:5f29d5e818fa63ca3e5034629bfc90deff3acada8a403255d2afdf416ef0ea82" + } + ] +} diff --git a/docs/agents/skill-dispatch.md b/docs/agents/skill-dispatch.md new file mode 100644 index 00000000..c388f076 --- /dev/null +++ b/docs/agents/skill-dispatch.md @@ -0,0 +1,243 @@ + + +# Skill dispatch + +This setup-owned guide is generated from the active modules. Activate every +matching skill before governed work; repository-authored dispatch extensions +may add stricter triggers. + +- **mandatory**: Activate every matching required skill before governed work. When one skill has distinct active-module triggers, retain and follow each trigger. + +- **mandatory**: Use the governing `conventional-commits` skill before staging changes, writing commit messages, or preparing pull request titles. + +- **mandatory**: Use the governing `github-pr-workflow` skill before preparing, opening, updating, or handing off a pull request. + +Exact activation bundles: + +- `trigger.delivery`: Preparing commits or pull request delivery. + - `bundle.delivery`: `conventional-commits`, `github-pr-workflow` +- `trigger.qa`: Running final Spec QA. + - `bundle.qa`: `qa-gate`, `evidence-gate` + +Individual skill triggers: + +- `agentic-cli-design`: + - `trigger.cli-surface.agentic-cli-design`: Changing CLI flags, streams, exit codes, JSON, dry-run, non-interactive, or introspection behavior. +- `archive-spec`: + - `trigger.context-workflow.archive-spec`: Archiving a completed and QA-passed Spec. +- `brainstorming`: + - `trigger.context-workflow.brainstorming`: Starting creative feature or behavior design before implementation. +- `bubbletea`: + - `trigger.tui-surface.bubbletea`: Building or changing Bubble Tea models, commands, messages, or component composition. +- `business-analyst`: + - `trigger.context-workflow.business-analyst`: Evaluating product viability, options, KPIs, or business trade-offs. +- `coding-guidelines`: + - `trigger.core.coding-guidelines`: Writing, modifying, refactoring, or reviewing implementation code. +- `context7`: + - `trigger.core.context7`: Consulting authoritative current documentation for a library, framework, runtime, or toolchain. +- `conventional-commits`: + - `trigger.core.conventional-commits`: Staging changes, writing commit messages, or preparing pull request titles. +- `council`: + - `trigger.context-workflow.council`: Debating a high-impact product or architecture decision through multiple advisors. +- `domain-modeling`: + - `trigger.context-workflow.domain-modeling`: Defining or changing domain vocabulary, ownership, or bounded-context relationships. +- `evidence-gate`: + - `trigger.core.evidence-gate`: Making any completion, readiness, or handoff claim. +- `github-pr-workflow`: + - `trigger.core.github-pr-workflow`: Preparing, opening, updating, or handing off a pull request. +- `golang-cli`: + - `trigger.go.golang-cli`: Changing Go command structure, flags, package layout, version output, or command tests. +- `golang-concurrency`: + - `trigger.go.golang-concurrency`: Using goroutines, channels, locks, worker pools, or other concurrent Go behavior. +- `golang-context`: + - `trigger.go.golang-context`: Designing Go IO boundaries, cancellation, timeouts, deadlines, or request-scoped values. +- `golang-error-handling`: + - `trigger.go.golang-error-handling`: Creating, wrapping, matching, logging, or exposing Go errors. +- `golang-lint`: + - `trigger.go.golang-lint`: Changing Go lint configuration or adding a lint suppression. +- `golang-testing`: + - `trigger.go.golang-testing`: Writing or reviewing Go tests, fixtures, fuzz tests, or integration tests. +- `grill-with-docs`: + - `trigger.context-workflow.grill-with-docs`: Stress-testing a plan or decision against repository documents. +- `grilling`: + - `trigger.context-workflow.grilling`: Stress-testing an idea or plan through direct questions. +- `handoff`: + - `trigger.core.handoff`: Recording resumable session state for another Agent or maintainer. +- `implement-spec`: + - `trigger.autonomous-work.implement-spec`: Delegating a complete autonomous Spec Task Graph. + - `trigger.context-workflow.implement-spec`: Executing every pending Task in a Spec Task Graph. +- `implement-task`: + - `trigger.autonomous-work.implement-task`: Delegating one autonomous Task to an ACP Runtime. + - `trigger.context-workflow.implement-task`: Executing exactly one assigned Spec Task. +- `knowledge-workspace`: + - `trigger.context-workflow.knowledge-workspace`: Consulting the configured knowledge workspace for relevant prior context. +- `no-workarounds`: + - `trigger.core.no-workarounds`: Fixing defects, regressions, or verification failures at their root cause. +- `qa-gate`: + - `trigger.context-workflow.qa-gate`: Running final Spec QA after implementation Tasks complete. +- `review`: + - `trigger.core.review`: Reviewing a change against repository standards and its originating contract. +- `setup-context-driven`: + - `trigger.context-workflow.setup-context-driven`: Auditing, applying, or refreshing the Context-Driven Baseline. +- `systematic-debugging`: + - `trigger.core.systematic-debugging`: Diagnosing a bug, failing check, or unexpected behavior before proposing a fix. +- `tech-writer`: + - `trigger.core.tech-writer`: Writing or revising technical documentation, Specs, findings, or delivery notes. +- `testing-boss`: + - `trigger.core.testing-boss`: Writing, changing, or reviewing tests and fixtures. +- `the-fool`: + - `trigger.context-workflow.the-fool`: Challenging a proposal through a pre-mortem, red-team, or evidence audit. +- `tui-design`: + - `trigger.tui-surface.tui-design`: Designing terminal layout, navigation, keyboard, mouse, color, or accessibility behavior. +- `write-idea`: + - `trigger.context-workflow.write-idea`: Expanding a product-level idea into a researched opportunity artifact. +- `write-prd`: + - `trigger.context-workflow.write-prd`: Defining product requirements and user outcomes. +- `write-tasks`: + - `trigger.context-workflow.write-tasks`: Decomposing an approved Spec into a dependency-ordered Task Graph. +- `write-techspec`: + - `trigger.context-workflow.write-techspec`: Settling implementation architecture and technical contracts. + + + + +## Repository dispatch extensions + +- **Broader web or source research**: Use `exa-web-search` when current + authoritative library documentation is insufficient. Search from multiple + angles and validate conclusions against primary sources. Never use it to + discover or infer local repository code. +- **Skill optimization**: Use `autoresearch` only to improve a repo-owned + skill through its evaluation loop. Never use it to mutate an + upstream-managed skill locally. + +### Skill ownership + +Repo-owned workflow skills are plain files under `.agents/skills/` and are not +listed in `skills-lock.json`; they may be adapted to Roundfix. Skills declared +in `skills-lock.json` are upstream-managed and must not be edited locally. +Propose needed changes to their upstream source. + +### Roundfix skill synchronization + +Before opening a PR, confirm `.agents/skills/roundfix/SKILL.md` matches the +shipped CLI commands, flags, output formats, exit codes, and Batch semantics. +When CLI behavior changes, update that canonical skill in the same PR and run +`make skills-sync` so the embedded `skills/roundfix/` copy matches it. + +The skill's `metadata.version` follows the released CLI version. Do not keep a +detached machine-global copy that can shadow this repository's canonical +contract; use a symlink to `skills/roundfix/` when a global install is needed. + + + +- **Feature discovery or product idea**: Use `brainstorming`; product-level + ideas go through `write-idea` (scored by `business-analyst`, debated by + `council`, challenged by `the-fool`) + + + + +- **PRD, tech spec, or task breakdown**: Use `write-prd`, `write-techspec`, + `write-tasks` + + + + +- **Executing spec tasks**: Use `implement-task` (one task) or `implement-spec` + (the whole graph in dependency order) + + + + +- **Final QA of a completed spec**: Use `qa-gate`; archive after release with + `archive-spec` + + + + +- **MANDATORY** for CLI behavior, flags, stdout/stderr, exit codes, JSON + output, dry-run behavior, non-interactive mode, or introspection: + `agentic-cli-design` + + + + +- **ALWAYS USE** `golang-cli` before writing Go command behavior, package + layout, version output, or command tests. CLI style is stdlib `flag.FlagSet` + dispatch with a `Run() int` exit-code contract — **no Cobra**. + + + + +- **ALWAYS USE** `golang-error-handling` for error paths: `%w` wrapping, + `errors.Is`/`As`, sentinels + + + + +- **ALWAYS USE** `golang-concurrency` before goroutines, channels, worker + pools, or anything with leak/race exposure + + + + +- **ALWAYS USE** `golang-context` for context propagation, cancellation, and + timeouts + + + + +- **Lint config or nolint**: Use `golang-lint` (golangci-lint, vet, + staticcheck discipline) + + + + +- **Tests, fixtures, golden files, integration tests**: Use `golang-testing` + plus `testing-boss` + + + + +- **Bubble Tea or Lip Gloss TUI work**: Use `bubbletea` and `tui-design` + + + + +- **Implementation**: Use `coding-guidelines` + + + + +- **Bug fix or failing test**: Use `no-workarounds` plus `systematic-debugging` + + + + +- **Docs, PRDs, ADRs, issues, PR descriptions**: Use `tech-writer` + + + + +- **Commits or PR titles**: Use `conventional-commits` + + + + +- **Completion claim**: Use `evidence-gate` + + + + +- **Session handoff**: Use `handoff` + + + + +- **Roundfix dogfooding or assigned-Batch contract checks**: Use `roundfix` + when driving Roundfix against an Open Pull Request or validating the Batch + resolution contract + + + diff --git a/docs/agents/skill-governance.md b/docs/agents/skill-governance.md deleted file mode 100644 index bd916e6e..00000000 --- a/docs/agents/skill-governance.md +++ /dev/null @@ -1,45 +0,0 @@ -# Skill governance - -Who owns each skill in this repo, the roundfix skill-sync contract, and where workflow rules -live. `AGENTS.md` keeps only short mandatory pointers to these rules; the bodies below are -canonical. - -## Rule placement - -HARD RULEs and workflow orientation for the CONTEXT-driven spec workflow and for Roundfix live -in the skills themselves and in `docs/agents/*.md` files — this one, -`docs/agents/autonomous-work.md`, `docs/agents/spec-routing.md`, `docs/agents/issue-tracker.md`, -`docs/agents/domain.md`. `AGENTS.md` holds a one-line pointer that marks each rule as mandatory, -never the full rule body. When a rule outgrows its pointer, move the body here or into the -matching skill and leave the pointer behind. - -## Roundfix skill sync (pre-PR gate) - -Before opening any PR, confirm `.agents/skills/roundfix/SKILL.md` still matches the shipped CLI -behavior: commands, flags, output formats, exit codes, Batch contract semantics. If the PR -changes any of those, the skill update ships in the same PR. - -- The project-local copy at `.agents/skills/roundfix/` is canonical. -- Its `metadata.version` tracks the released CLI version (the `v*` tag), not an independent - skill version. -- The embedded `skills/roundfix/` is generated from it with `make skills-sync`; `make verify` - fails on drift. -- Never keep a detached global copy (for example `~/.claude/skills/roundfix/`) — a stale copy - shadows the canonical one and teaches agents a contract that no longer exists (observed - 2026-07-15, findings retrospective §2). If a machine-global install is wanted, symlink it to - this repository's `skills/roundfix/`. - -## Skill ownership - -The authorial context-workflow skills are vendored in this repository and owned by it: -`write-idea`, `write-prd`, `write-techspec`, `write-tasks`, `setup-context-driven`, `implement-task`, -`implement-spec`, `brainstorming`, `council`, `business-analyst`, `archive-spec`, `qa-gate`, -and `evidence-gate`. - -- They live in `.agents/skills/` as plain repo files and are **never** listed in - `skills-lock.json`. -- They may be adapted to Roundfix's needs — changes ship like any other repo change. -- Every other skill (including `grilling`, `grill-with-docs`, `the-fool`, `domain-modeling`, - and all generic engineering skills) remains managed by the external `marcioaltoe/skills` - origin through `skills-lock.json` and **must not** be modified locally — needed changes go - upstream. diff --git a/docs/agents/spec-routing.md b/docs/agents/spec-routing.md index d75af696..a834aa58 100644 --- a/docs/agents/spec-routing.md +++ b/docs/agents/spec-routing.md @@ -37,3 +37,44 @@ to continue from the files without relying on prior conversation. - `qa-gate` validates the assembled behavior and writes evidence under `qa/`. - On QA pass, `archive-spec` stamps and moves the Spec to `_archived/`. - Merge and release remain separate user-directed actions. + + + +# Spec routing + +- **mandatory**: Large or fuzzy product initiative: run `write-idea` → `write-prd` → `write-techspec` → `write-tasks`. + +- **mandatory**: Standard feature that changes product behavior: run `write-prd` → `write-techspec` → `write-tasks`; skip the TechSpec only when the feature has no architectural surface. + +- **mandatory**: Refactor or bug fix without product-behavior change: run `write-techspec` → `write-tasks` and create the minimal `_prd.md` required by the downstream artifact contract. + +- **mandatory**: Trivial one-line fix, typo, or configuration tweak: implement directly without a Spec only when intent, acceptance criteria, and Verification are obvious. + +- **mandatory**: Use `brainstorming` before creative or feature work, start with the smaller sufficient route when two routes fit, and execute implementation from the Task Graph. + +- **mandatory**: Before producing a Task Graph, require every active, non-archived, and not already completed Spec PRD and present TechSpec to contain complete Project Constraints: applicability with reasons for identifier strategy, authentication and HTTP, active ADR obligations, and tooling authority, each citing its operative `docs/agents/` source. + +- **mandatory**: Refuse a tooling Task unless the active PRD and present TechSpec record express maintainer authorization and the exact bounded repository-relative files; Task assignment, setup approval, or generic implementation approval is not authorization. + +- **mandatory**: An authorized tooling Task may mutate only its bounded repository-relative files and its own Task file; stop before any other mutation and fail the Task if changed-file postflight finds another path. + +- **mandatory**: Final QA verifies Project Constraint applicability, operative source paths, tooling authorization, and actual changed-file scope from Git evidence; missing authorization, untraceable scope, or out-of-scope tooling changes fails the gate. + +- **mandatory**: Keep completed or archived legacy Specs byte-identical. Dependencies remain owned only by the Task Graph, and status remains owned only by each Task file. + + + + +### Spec routing + +Pick the pipeline entry point by the change — large initiative, feature, +refactor/bugfix, or trivial. See `docs/agents/spec-routing.md`. + + + + + +4. Asking for confirmation before running spec tasks — invocation is the + authorization + + diff --git a/docs/agents/specific-repository.md b/docs/agents/specific-repository.md new file mode 100644 index 00000000..fae5b6ce --- /dev/null +++ b/docs/agents/specific-repository.md @@ -0,0 +1,62 @@ +# Agent instructions + +This repository is Roundfix: a local-first Go CLI and future daemon that picks +up work items (today: PR review issues; next: spec task graphs), resolves them +through the user's selected ACP runtime, and pushes only when nothing +unresolved remains. Stdlib `flag` dispatch and a Bubble Tea v2 TUI. + + Any format failure, test failure, or build failure is **blocking** — zero + tolerance. CI validates PR titles only; the local gate is the ONLY gate. +- Keep the project KISS: prefer the smallest behavior that satisfies the + documented product contract. +- **NEVER** copy names, branding, package names, comments, examples, or + generated artifacts from reference projects into this repository. +- Agent-created branches **MUST** use the `ma/` prefix. +- **HARD RULE — roundfix skill sync**: before opening any PR, confirm the + roundfix skill still matches the shipped CLI behavior; a PR that changes + CLI behavior ships the skill update too. Contract: + `docs/agents/skill-dispatch.md`. +- **HARD RULE — skill ownership**: repo-owned authorial workflow skills may + be adapted locally; every other skill is upstream-managed and **MUST NOT** + be modified here. Ownership split: `docs/agents/skill-dispatch.md`. +- **HARD RULE — release planning**: release work starts with the read-only + `roundfix release plan` before changelog, version, tag, push, package, asset, + or GitHub Release mutation. A generic release request authorizes only a + conclusive patch plan; minor, major, version-zero breaking, and manual + classification outcomes require the decisions in + `docs/user-guide/release-runbook.md`. +- Project map: `cmd/roundfix/` is the thin CLI entry point; behavior lives in + `internal/...` (`internal/cli/` owns parsing, output, and exit behavior; + `internal/app/` holds app metadata) + +### Skill dispatch + +Skill triggers, ownership, and the Roundfix skill-sync contract live in +`docs/agents/skill-dispatch.md`. + +## Verification + +The full gate is `make verify` (fmt-check + test + `roundfix skills check` + +build) and it **MUST** pass 100% before any completion claim. For the smallest +relevant gate while iterating: + +```bash +rtk gofmt -w +rtk go test ./... +rtk go run ./cmd/roundfix --help +``` + +If concurrency changed, also run: + +```bash +rtk go test -race ./... +``` + +- Use `conventional-commits` for commits and PR titles (check `cog.toml`). +- Commit and PR titles are unscoped Conventional Commits subjects here + (`cog.toml` sets `scopes = []`). +- PR bodies summarize changes, call out risk, and list validation commands run. + +## Anti-patterns (immediate rejection) + +1. Introducing Cobra, testify, or any dependency the stdlib covers diff --git a/docs/agents/tui.md b/docs/agents/tui.md new file mode 100644 index 00000000..eb29d58d --- /dev/null +++ b/docs/agents/tui.md @@ -0,0 +1,18 @@ + + +# TUI surface + +- Drive TUI model updates synchronously and assert rendered state, messages, and transitions. Use terminal emulation only when model-level tests cannot prove the behavior, and keep layout and interaction policy in repository-owned design guidance. + + + + +- TUI code uses **Bubble Tea v2 module paths** (`charm.land/bubbletea/v2`, + `charm.land/lipgloss/v2`) and the v2 API (`tea.KeyPressMsg`, `tea.Key`). + + + + + Drive `model.Update(...)` synchronously in tests — no terminal emulation. + + diff --git a/docs/findings/2026-07-25-npm-trusted-publishing-and-release-preflight.md b/docs/findings/2026-07-25-npm-trusted-publishing-and-release-preflight.md new file mode 100644 index 00000000..4c024bc0 --- /dev/null +++ b/docs/findings/2026-07-25-npm-trusted-publishing-and-release-preflight.md @@ -0,0 +1,93 @@ +--- +status: pending +created_at: 2026-07-25 +updated_at: 2026-07-25 +--- + +# npm publishing — token authentication and registry state can produce a partial release (2026-07-25) + +The `v0.0.1` release reset exposed two independent publication risks. Roundfix +still authenticates npm publication with a long-lived repository secret, and +the release workflow does not prove that every package can accept the target +version before it begins publishing the platform packages. + +Session evidence: + +- Release Run + [`30166957746`](https://github.com/marcioaltoe/roundfix/actions/runs/30166957746) + failed in the Verification gate because + `TestBaselineApplyCommandRealCLI` used the macOS-only + `/private/tmp/roundfix-task09-go-cache` path on a Linux runner. +- Commit `f98a12fcb99015d37be1fb32c26a0e2e57fd0c6e` replaced that path with + a test-owned temporary directory, and the focused test and local + `make verify` passed. +- Release Run + [`30167217166`](https://github.com/marcioaltoe/roundfix/actions/runs/30167217166) + was canceled during its Verification gate before cross-compilation or + publication. +- At cancellation time, the unscoped `roundfix` package reported that the + whole package had been unpublished on `2026-07-25T17:14:13.763Z`. + The five `@roundfix/cli-*` packages remained published at `0.3.0`. +- npm blocks every new version of a fully unpublished package for 24 hours. + A previously published exact `package@version` can never be reused. See the + [npm unpublish policy](https://docs.npmjs.com/policies/unpublish/). + +## 1. The release workflow depends on a long-lived npm credential + +- **Symptom / evidence**: + [`.github/workflows/release.yml`](../../.github/workflows/release.yml) + exposes `secrets.NPM_TOKEN` as `NODE_AUTH_TOKEN` to six sequential + `npm publish --access public` commands. The + [release runbook](../user-guide/release-runbook.md) describes the secret as + an npm Automation token. +- **Root cause**: The publication contract predates npm Trusted Publishing. + Authentication is repository-wide secret state rather than a short-lived + identity bound to the GitHub repository, workflow file, and hosted runner. +- **Action / suggestion**: Migrate the release workflow to + [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/) with + GitHub Actions OIDC. Configure `marcioaltoe/roundfix` and `release.yml` as + the trusted publisher for the launcher and every platform package, grant + only the `npm publish` action, add `id-token: write`, and use supported Node + and npm versions. Keep `NPM_TOKEN` until one complete release proves the + OIDC path; then remove the workflow secret and disallow token publication + for the owned packages. + +## 2. Publication starts before registry eligibility is established + +- **Symptom / evidence**: The workflow publishes every `@roundfix/cli-*` + package before publishing `roundfix`. During the reset, the platform + packages could accept `0.0.1`, but the launcher name was inside npm's + 24-hour post-unpublish block. Without cancellation, publication could have + stopped after creating five installable platform packages but before + creating the launcher or GitHub Release. +- **Root cause**: `Validate tag` checks SemVer and the checked-in owned + version, but no read-only step checks all registry coordinates, the exact + target version, an unpublish cooldown, or package ownership before the first + irreversible publish. +- **Action / suggestion**: Add a publication preflight after Verification and + before cross-compilation. It must evaluate the launcher and all platform + packages as one release set, refuse an already-used target version, detect a + package-name cooldown or missing ownership, and emit the exact blocked + coordinate. No package may publish unless every coordinate is eligible. + +## 3. Migration must preserve the existing release contract + +- **Symptom / evidence**: The release workflow stamps every binary and npm + package from the Git tag, publishes platform packages before the launcher, + and creates the GitHub Release last. The ordering protects launcher + installation only when the full npm set is eligible. +- **Root cause**: Authentication migration and release-set validation affect + the same irreversible boundary, but neither requires a new package layout + or version authority. +- **Action / suggestion**: Preserve the Git tag as the release version + authority, the existing package names, platform asset names, changelog + extraction, and GitHub Release assets. Treat release-set preflight and OIDC + authentication as separate Tasks so a failure identifies whether registry + state or identity blocked publication. + +## Routing + +A future Spec must define the npm preflight contract, OIDC migration, +credential rollback window, package-by-package trusted-publisher setup, and +real release validation. This finding does not authorize renaming packages, +reusing unpublished versions, or changing the current `v0.0.1` tag. diff --git a/docs/findings/2026-07-25-spec-owned-reference-lifecycle.md b/docs/findings/2026-07-25-spec-owned-reference-lifecycle.md new file mode 100644 index 00000000..74d23902 --- /dev/null +++ b/docs/findings/2026-07-25-spec-owned-reference-lifecycle.md @@ -0,0 +1,117 @@ +--- +status: pending +created_at: 2026-07-25 +updated_at: 2026-07-25 +--- + +# Spec workflow — adopted source documents must travel with their owning Spec (2026-07-25) + +The repository describes inbox-to-finding-to-Spec promotion as the normal +document lifecycle, but promotion currently creates links without transferring +the source document. A Spec can therefore be complete and archived while the +evidence that shaped it remains in a separate lifecycle with links that no +longer resolve. + +Session evidence: + +- [`docs/agents/docs-layout.md`](../agents/docs-layout.md) says + `inbox → findings → spec` is the normal flow and that a file must move when + its job changes. +- The same guide treats `docs/findings/` as immutable history with addenda and + does not define how a promoted Finding becomes Spec-owned evidence. +- [`2026-07-23-setup-context-driven-adoption-process-improvements.md`](2026-07-23-setup-context-driven-adoption-process-improvements.md) + still links Specs 0045 and 0046 under `docs/specs/`, although both Specs now + live under `docs/specs/_archived/`. +- [`2026-07-24-spec-implementation-sequence.md`](../_inbox/2026-07-24-spec-implementation-sequence.md) + remained in the inbox after the Specs it routed were implemented and + archived. +- Archived Specs 0035, 0040, and 0041 already contain a `references/` + directory, but no authorial workflow defines when or how source documents + enter it. + +## 1. Linked source documents remain outside the Spec lifecycle + +- **Symptom / evidence**: PRDs and Findings can link each other while living + under independent paths. Archiving moves the entire Spec directory but does + not update external Finding or inbox links, so the traceability chain + degrades after successful completion. +- **Root cause**: The authorial skills create and archive Spec artifacts, but + no step assigns ownership of an adopted inbox note or Finding, moves it into + the Spec, or validates links after the archive. +- **Action / suggestion**: Make source adoption an explicit Spec lifecycle + transition. Once a Spec accepts a document as an implementation source, the + Spec becomes its sole owner and the document moves with Git history to + `docs/specs//references/`. + +## 2. The move must preserve provenance without duplicating content + +- **Symptom / evidence**: Copying a Finding into a Spec would leave two + authoritative documents that can diverge. Leaving a stub at the original + path would preserve a second artifact whose lifecycle and archive behavior + remain ambiguous. +- **Root cause**: The current workflow has link semantics but no ownership, + provenance, or deduplication contract for Spec references. +- **Action / suggestion**: Use one move, not a copy: + + 1. During PRD creation or promotion, inventory every adopted source from + `docs/_inbox/` and `docs/findings/`. + 2. Assign exactly one owning Spec to each source. + 3. Move the file to `docs/specs//references/` without changing its + basename or historical content. A Finding records `status: done` and its + Spec link in the same change before it moves. + 4. Record the original path, source type, owning Spec, adoption date, and + current relative path in a Spec-local reference index. + 5. Rewrite repository links to the new location and fail the authoring gate + when an adopted source remains at its original path or a link is broken. + 6. Archive the Spec directory as one unit; its PRD, TechSpec, Tasks, QA, and + source references then retain stable relative links. + +## 3. Authorial and archive workflows need one shared contract + +- **Symptom / evidence**: `write-idea`, `write-prd`, `write-techspec`, + `write-tasks`, `qa-gate`, and `archive-spec` consume adjacent parts of the + traceability chain, but none owns the source-document transfer. +- **Root cause**: Reference collection is treated as document linking rather + than a state transition with an owner and validation gate. +- **Action / suggestion**: A future Spec must update the repository-owned + authorial skills, `docs/agents/docs-layout.md`, + `docs/agents/spec-routing.md`, and the Spec validation contract together. + `write-prd` is the recommended owner of the move because that is the first + stage where the repository commits to implementing the accepted scope. + `archive-spec` must validate that every declared source is already inside + the Spec and that no source link points back to `docs/_inbox/` or + `docs/findings/`. + +## 4. Shared and historical sources require explicit decisions + +- **Symptom / evidence**: One source can influence multiple Specs, and existing + active or archived Specs already link documents outside their folders. +- **Root cause**: A physical move can have only one destination, while + cross-Spec evidence can have more than one consumer. +- **Action / suggestion**: The future Spec must decide: + + - how one primary owning Spec is selected when multiple Specs consume the + same source; + - how secondary Specs link the owning Spec without creating a duplicate; + - whether already archived Specs and historical Findings are migrated or + only new promotions use the workflow; + - whether raw inbox content must first become a Finding or can move directly + into a Spec after its reliability and scope are reviewed; and + - how repository-wide discovery finds Findings after they move into archived + Spec references. + +## What worked — keep + +- Keep one purpose per `docs/` folder. +- Keep Findings evidence-first and append-only; moving a file must not rewrite + the observations it records. +- Keep Spec directories portable so archiving remains one directory move. +- Keep downstream documents linked to upstream sources rather than duplicating + their content. + +## Routing + +This finding proposes a new Spec-authoring workflow and remains `pending` until +an implementation Spec defines its source inventory, ownership rules, +reference-index schema, migration boundary, validation errors, skill changes, +and archive behavior. diff --git a/docs/specs/0036-doctor-skill-readiness/_prd.md b/docs/specs/0036-doctor-skill-readiness/_prd.md index 400aef37..36a80369 100644 --- a/docs/specs/0036-doctor-skill-readiness/_prd.md +++ b/docs/specs/0036-doctor-skill-readiness/_prd.md @@ -155,8 +155,9 @@ authorization, and rerun Doctor to prove the result. - Setup snapshot synchronization resolves digest authority from `source.type`; repository-owned entries never derive their digest from the external setup checkout. -- This change extends the existing Doctor Command and skill-governance - contracts; it does not require a new architectural decision record. +- This change extends the existing Doctor Command plus the skill ownership and + synchronization contracts in `docs/agents/skill-dispatch.md`; it does not + require a new architectural decision record. ## Dependencies diff --git a/docs/specs/0036-doctor-skill-readiness/_techspec.md b/docs/specs/0036-doctor-skill-readiness/_techspec.md index 00905dfa..2fffdd68 100644 --- a/docs/specs/0036-doctor-skill-readiness/_techspec.md +++ b/docs/specs/0036-doctor-skill-readiness/_techspec.md @@ -289,5 +289,7 @@ integration before starting this graph. - `sync-setups` preserves current valid digests for `source.type: repo` and never hashes a conflicting external checkout for those entries. - One aggregate `skills:` line preserves Doctor's compact public surface. -- No ADR is added because this is an extension of the accepted Doctor and - skill-governance boundaries, not a new hard-to-reverse architecture choice. +- No ADR is added because this is an extension of the accepted Doctor plus the + skill ownership and synchronization boundaries in + `docs/agents/skill-dispatch.md`, not a new hard-to-reverse architecture + choice. diff --git a/docs/specs/0036-doctor-skill-readiness/task_01.md b/docs/specs/0036-doctor-skill-readiness/task_01.md index e690a91a..0f4083cd 100644 --- a/docs/specs/0036-doctor-skill-readiness/task_01.md +++ b/docs/specs/0036-doctor-skill-readiness/task_01.md @@ -86,7 +86,7 @@ filesystem mutation. ## Context - instruction: `CONTEXT.md` -- instruction: `docs/agents/skill-governance.md` +- instruction: `docs/agents/skill-dispatch.md` - instruction: `docs/specs/_archived/0041-agent-selection-runtime-readiness/_techspec.md` - interface: `skills/skills.go` - interface: `skills/skills_test.go` @@ -117,7 +117,7 @@ filesystem mutation. - `docs/specs/_archived/0041-agent-selection-runtime-readiness/_techspec.md` → prerequisite profile-aware Doctor coordinator and output order. - `CONTEXT.md` → Doctor Command; Repository Skill Set; Roundfix Skill. -- `docs/agents/skill-governance.md` → owned versus external authority and +- `docs/agents/skill-dispatch.md` → owned versus upstream-managed authority and synchronization boundaries. ## Result diff --git a/docs/specs/0036-doctor-skill-readiness/task_02.md b/docs/specs/0036-doctor-skill-readiness/task_02.md index 27cbe1d1..87df19ba 100644 --- a/docs/specs/0036-doctor-skill-readiness/task_02.md +++ b/docs/specs/0036-doctor-skill-readiness/task_02.md @@ -72,7 +72,7 @@ failure, and remediation contract as the shipped CLI. ## Context - instruction: `CONTEXT.md` -- instruction: `docs/agents/skill-governance.md` +- instruction: `docs/agents/skill-dispatch.md` - docs: `README.md` - docs: `docs/user-guide/commands.md` - docs: `docs/user-guide/usage.md` @@ -103,8 +103,8 @@ failure, and remediation contract as the shipped CLI. - `task_01.md` → the shipped Doctor behavior this guidance must describe. - `docs/specs/_archived/0041-agent-selection-runtime-readiness/_prd.md` → profile-aware Doctor prerequisite and separation of readiness concerns. -- `docs/agents/skill-governance.md` → canonical/embedded sync and external - ownership rules. +- `docs/agents/skill-dispatch.md` → canonical/embedded sync and + upstream-managed ownership rules. ## Result diff --git a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_07.md b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_07.md index 059e4f5f..8df8ad08 100644 --- a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_07.md +++ b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_07.md @@ -33,8 +33,9 @@ must match shipped behavior and preserve skill ownership boundaries. 6. MUST update the canonical Roundfix Skill and repository-owned `implement-task` skill, regenerate the embedded Roundfix Skill, and leave every upstream-managed skill untouched. -7. MUST preserve ADR, finding, PRD, Tech Spec, glossary, and skill-governance - traceability and keep all repository content in English. +7. MUST preserve ADR, finding, PRD, Tech Spec, glossary, and skill ownership + and synchronization traceability and keep all repository content in + English. 8. MUST run the complete repository verification gate; any format, test, skill synchronization, or build failure is blocking. @@ -69,7 +70,7 @@ must match shipped behavior and preserve skill ownership boundaries. ## Context -- instruction: `docs/agents/skill-governance.md` +- instruction: `docs/agents/skill-dispatch.md` - instruction: `docs/agents/autonomous-work.md` - instruction: `.agents/skills/tech-writer/SKILL.md` - instruction: `.agents/skills/agentic-cli-design/SKILL.md` diff --git a/docs/specs/0049-baseline-preservation-idempotency/_prd.md b/docs/specs/0049-baseline-preservation-idempotency/_prd.md new file mode 100644 index 00000000..474c5dcf --- /dev/null +++ b/docs/specs/0049-baseline-preservation-idempotency/_prd.md @@ -0,0 +1,72 @@ +--- +spec: 0049-baseline-preservation-idempotency +status: active +created: 2026-07-26 +surfaces: [cli, infra] +--- + +# Baseline Preservation idempotency + +## Problem + +After an approved Preservation semantically redistributes existing root +instructions, a fresh Baseline Plan inventories the original root bytes and +recognized repository-rule carriers again. It can therefore propose another +backup, duplicate retained rules, and update generated guidance even though the +repository has not received new instructions. + +## Project Constraints + +- Identifier strategy: not applicable — this correction creates no Internal + Identifier or application identity. Source: `docs/agents/domain.md`. +- Authentication and HTTP: not applicable — the correction is confined to + local Baseline planning and changes no authentication provider, HTTP + contract, or route. Source: `docs/agents/cli.md`. +- Active ADR obligations: applicable — byte-exhaustive accounting, + fail-closed retention, bounded carrier mutation, portable planning, and + recoverable apply remain binding; ADR-0078 replaces only the live-root + retention outcome after confirmed redistribution. Source: + `docs/agents/domain.md`. +- Tooling authority: applicable — no protected tooling mutation is proposed or + authorized. Source: `docs/agents/agent-instructions.md`. + +## Goals + +- Make the first approved Preservation the sole owner-transfer operation for + the root bytes it accounts for. +- Make an unchanged subsequent Preservation produce zero file changes and no + new backup. +- Preserve strict accounting, confirmation, and rollback guarantees when new + unmarked instructions are later added to `AGENTS.md`. + +## User Stories + +1. As a maintainer, I want redistributed root rules removed from the live root + carrier, so that every accepted rule has one operative owner. +2. As a maintainer, I want an unchanged Preservation rerun to be empty, so that + updating the Baseline is safe and predictable. +3. As a maintainer adding new root instructions, I want only those new bytes + reviewed and migrated, with an immutable backup of that source version. + +## Core Features + +1. Preservation classification excludes setup-managed blocks and already + retained repository-owned rule carriers. +2. An approved Preservation postimage rebuilds `AGENTS.md` from active managed + root blocks and confirmed root-level repository-owned content, excluding + source bytes whose dispositions were accepted. +3. The immutable backup contains the complete pre-migration root carrier and + remains addressed by its content digest. +4. A new unmarked addition produces one new review and one backup for its new + complete root content identity; after apply, the live root converges again. +5. Apply and recovery continue to validate exact preimages, postimages, + dispositions, retention entries, and backup identities. + +## Non-Goals + +- Rewriting or deleting arbitrary nested instruction carriers. +- Changing the public Baseline Plan, Result, confirmation, or exit-code + schemas. +- Reclassifying repository-owned semantic blocks that already have an accepted + owner. +- Changing Baseline Profile content or protected repository tooling. diff --git a/docs/specs/0049-baseline-preservation-idempotency/_tasks.md b/docs/specs/0049-baseline-preservation-idempotency/_tasks.md new file mode 100644 index 00000000..d9a41f3e --- /dev/null +++ b/docs/specs/0049-baseline-preservation-idempotency/_tasks.md @@ -0,0 +1,17 @@ +--- +schema: spec-tasks/v1 +spec: 0049-baseline-preservation-idempotency +graph: + nodes: + - id: task_01 + file: task_01.md + needs: [] +--- + +# Tasks — Baseline Preservation idempotency + +| id | title | type | complexity | needs | +| ------- | --------------------------------------------------------------- | ------- | ---------- | ----- | +| task_01 | Make Baseline Preservation converge after semantic redistribution | backend | high | — | + +Waves: 1 → task_01 diff --git a/docs/specs/0049-baseline-preservation-idempotency/_techspec.md b/docs/specs/0049-baseline-preservation-idempotency/_techspec.md new file mode 100644 index 00000000..ffdf4e12 --- /dev/null +++ b/docs/specs/0049-baseline-preservation-idempotency/_techspec.md @@ -0,0 +1,263 @@ +--- +spec: 0049-baseline-preservation-idempotency +prd: _prd.md +created: 2026-07-26 +--- + +# Baseline Preservation idempotency — Technical Spec + +## Executive Summary + +This correction separates the bytes that require Preservation classification +from the complete root preimage that requires immutable backup. The +preservation engine will expose only new unmarked root spans as Source Baseline +Entries; setup-managed blocks and repository-owned rules already represented +by the retention inventory will not re-enter classification. After every +source entry has an approved disposition, portable Plan assembly will rebuild +the active `AGENTS.md` from its owned managed and repository-owned regions +instead of appending those regions to the consumed source. The design derives +root consumption only from the ready Source Baseline and its complete +disposition set so the apply contract can remain strict and unchanged. + +## Project Constraints + +- Identifier strategy: not applicable — the implementation creates no + Internal Identifier or application identity. Source: + `docs/agents/domain.md`. +- Authentication and HTTP: not applicable — only local Baseline inventory, + Plan assembly, and filesystem postimages change. Source: + `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADRs 0058, 0060, 0064, 0069, 0071, + 0073, 0074, and 0078 require exhaustive accounting, supervised semantic + decisions, portable preimage-bound Plans, recoverable apply, and + single-owner rule retention. ADR-0070 still limits mutation to root carriers + and leaves arbitrary nested carriers warning-only. Source: + `docs/agents/domain.md`. +- Tooling authority: applicable — no protected tooling mutation is proposed or + authorized. Source: `docs/agents/agent-instructions.md`. + +## System Architecture + +The correction extends the existing `internal/baseline` preservation and Plan +assembly seams. It adds no package, public command, dependency, or persistence +schema. + +`PlanRootPreservation` continues to load complete root carrier bytes and derive +their content identities for immutable backups. Its classification projection, +however, contains only unmarked spans with nonblank repository-authored bytes. +Setup-managed spans remain structural evidence but are omitted from the Source +Baseline presented to semantic analysis. Recognized +`docs/agents/specific-repository.md` and repository-owned blocks in semantic +guides remain under `planSpecificRepository` and +`inventoryRepositoryRuleBlocks`. A Setup Manifest whose catalog and Profile +identities still resolve proves those carriers already have repository +ownership, so they are not independent migration sources. An absent, +incompatible, or stale Setup Manifest continues to expose recognized carriers +for explicit Readoption. + +For repositories already written by the defective apply behavior, a compatible +Setup Manifest plus a verified `AGENTS..md` payload proves that the +matching unmarked root bytes were part of an earlier approved transfer. Those +bytes are removed from the classification projection and the live stale copy +is consumed without asking ACP to make a second, potentially different, +classification. If new unmarked bytes coexist with that backed-up payload, +only the new remainder enters classification. + +`assemblePostimages` consumes a preservation-owned root path only after its +current Source Baseline has complete valid dispositions or its stale payload is +proven by the compatible Manifest and verified prior backup. For `AGENTS.md`, +it starts that postimage from the managed regions that remain active rather +than from the full preimage, then applies the normal managed-block and +repository-rule assembly. This removes classified or previously approved +source bytes without weakening backup or retention validation. A root with no +new unmarked bytes and no stale payload is not consumed again and produces no +backup or classification work. + +The existing portable Plan, `validatePlanApplyContract`, recoverable +transaction, and preimage comparison remain the mutation authority. + +## Implementation Design + +### Interfaces + +No request field or persisted signal is added. Plan assembly treats a root path +as consumed only when: + +1. it is a writable root source selected by preservation inventory; +2. it contains at least one nonblank unmarked Source Baseline Entry; +3. every current entry passes the existing classification and disposition + validation; and +4. its complete current content has a valid immutable backup plan. + +A repair path may also consume a root whose unmarked payload exactly matches a +verified content-addressed backup and whose current Setup Manifest resolves the +same catalog and Profile ownership. This evidence is kept only in the +in-memory preservation result; it adds no public Plan or Result field. + +A helper projects classification entries without changing backup identity: + +```go +func partitionUnmarkedRootSource( + path string, + content []byte, +) []ReadoptionSourceEntry +``` + +It uses the existing managed-marker parser and preserves original byte offsets, +digests, carrier digest, and source bytes for every admitted span. Empty +inter-block whitespace does not become a decision. Managed blocks remain +available to `containsOnlySetupManagedGuidance` and other ownership checks. + +### Data Models + +No persisted schema changes. `ReadoptionSourceBaseline.ByteCount` becomes the +sum of admitted Source Baseline Entry bytes rather than the full size of root +and recognized repository-rule carrier preimages. For every admitted carrier, +the Source Baseline identity still binds its complete current bytes, while its +entries bind the exact admitted ranges and bytes. Managed-byte drift and stale +classification proposals therefore fail closed without presenting managed +bytes as decisions. + +The backup continues to bind the complete carrier content identity. Therefore +one addition outside managed markers changes the digest-addressed backup path +even when all managed bytes are unchanged. An existing backup with the same +digest is reused; a new backup is planned only for a new complete carrier +identity. + +Already retained repository-owned semantic blocks are authoritative through +the Upgrade Retention Contract. The canonical +`docs/agents/specific-repository.md` is authoritative through +`planSpecificRepository`. Neither is copied into the Source Baseline on an +ordinary compatible Preservation update. When a later root rule also resolves +to Repository-Specific Normative Rules, the planner preserves the existing +carrier and appends the newly approved exact bytes. It replaces a recognized +carrier only when that carrier itself entered the reviewed Source Baseline. + +### API Contracts + +The public command shape and result schemas do not change. + +For a first or newly changed Preservation: + +1. audit reads the complete current carrier; +2. the human or automation flow reviews only new unmarked Source Baseline + Entries; +3. the Change Plan shows the complete digest-addressed backup, every + disposition and retention destination, and the rebuilt root postimage; +4. apply verifies the approved Plan Digest and exact preimages; +5. the live root contains only active generated hierarchy/pointers and any + explicitly retained root-owned regions. + +For an unchanged compatible Preservation, the Source Baseline has no new +entries. Planning reuses current decisions and retention evidence, emits no +backup, and the final Change Plan has zero file changes. + +If an unmarked byte cannot be accounted for, the existing action-required or +blocked outcome remains. Failure never strips the source or creates a backup. + +## Coverage Map + +- Goal 1 → unmarked-source projection and consumed-root postimage assembly. +- Goal 2 → fresh-plan integration journey and backup de-duplication assertions. +- Goal 3 → complete-preimage backup identity and later-addition journey. +- Story 1 → ready-disposition detection and root reconstruction. +- Story 2 → compatible unchanged Preservation test. +- Story 3 → new-unmarked-span classification, backup, apply, and second rerun + test. + +## Integration Points + +Semantic segmentation and classification continue to receive a sealed Analysis +Snapshot. The smaller Source Baseline does not grant new filesystem access or +change the preferred/fallback ACP Runtime contract. + +Plan assembly continues to use catalog artifacts, repository-rule inventory, +preimages, postimages, and the complete managed-entry ledger. +`validatePlanApplyContract` remains responsible for proving that every planned +root consumption has the exact immutable backup and that every postimage is +preimage-bound. Transaction recovery continues to restore the complete +preimage, including consumed root bytes. + +No user-guide or thin-skill update is required because the public contract +already promises semantic redistribution, immutable backup, and empty +reapply. A documentation contract test should instead prevent those existing +claims from drifting. + +## Testing Approach + +Unit tests for preservation inventory will cover managed-only roots, mixed +managed and unmarked roots, empty whitespace around managed blocks, malformed +markers, recognized residual carriers, and stable source identities. They will +assert that backup identity uses the complete carrier while classification +uses only admitted unmarked spans. + +Plan integration tests will begin with a repository whose `AGENTS.md` contains +real repository rules. They will build and apply an approved semantic +redistribution, assert the exact original bytes in +`AGENTS..md`, assert those bytes are absent from the live root and +present only in their selected owners, then build a fresh compatible +Preservation Plan and require zero postimage changes and no backup entry. + +A second journey will add a new unmarked rule to the managed-only root. It must +produce only that new classification input, create a backup for the new +complete root identity, consume the new source after apply, and converge to a +zero-change third Plan. A migration-repair journey will start with source bytes +already present in their semantic or residual owner, a valid Manifest, and the +original content-addressed backup. It will assert that the stale bytes do not +re-enter classification, consuming the live-root copy does not append a +duplicate block, and an adjacent new rule remains the sole classification +input. Existing tests continue to cover repository-owned semantic edits, +residual carrier edits, preimage drift, backup collisions, atomic rollback, +and recovery. + +## Build Order + +1. Add preservation inventory and Plan journey tests that reproduce duplicate + classification, second-backup creation, and live-root duplication. +2. Project only new unmarked root spans into the Source Baseline and derive + consumed root paths after complete disposition validation (depends on: 1). +3. Rebuild consumed `AGENTS.md` postimages from owned regions while preserving + exact backup, retention, preimage, rollback, and recovery contracts + (depends on: 2). +4. Add the later-unmarked-addition convergence journey and documentation + contract guard, then run full Verification (depends on: 3). + +## Risks & Considerations + +The primary risk is deleting a byte before it has a proven destination. +Consumption is therefore downstream of the existing exhaustive classification +and disposition validator and upstream of the unchanged preimage-bound apply +contract. A missing disposition, stale Source Baseline, backup collision, or +invalid semantic owner prevents the path from entering `ConsumedRootPaths`. + +Managed-marker corruption must continue to fail closed; an invalid marker +cannot be treated as harmless unmarked content and silently consumed. Literal +blank separators around managed blocks may be normalized by root +reconstruction, but nonblank bytes always require disposition. + +Repository-owned semantic blocks must not re-enter classification merely +because their bodies were edited. Their current bodies are already +repository-owned retention evidence. Direct unmarked additions to `AGENTS.md` +must still enter classification. A historical backup is repair evidence only +when its filename digest matches its exact bytes, the bytes are a contiguous +part of an unmarked root span, setup-managed guidance is also present, and the +current Manifest proves compatible ownership. Invalid, unrelated, or stale +backup files cannot suppress classification. +are different: they intentionally reopen Preservation and produce a new +content-addressed backup before migration. + +The correction deliberately applies to the generated `AGENTS.md` root. Safe +aliases that target it follow the same bytes. Independent root carriers without +a generated managed owner retain ADR-0070's existing preservation behavior +until a separately approved design defines their canonical postimage. + +## Decisions + +- Rebuild the live `AGENTS.md` after confirmed distribution instead of keeping + migrated source bytes beside their new owners. See ADR-0078. +- Classify only new nonblank unmarked root spans. +- Back up the complete current root content once per distinct digest. +- Keep recognized residual and semantic repository-rule carriers in the + existing retention inventory, not the Source Baseline. +- Preserve public Plan, Result, apply, recovery, confirmation, and exit-code + contracts. diff --git a/docs/specs/0049-baseline-preservation-idempotency/task_01.md b/docs/specs/0049-baseline-preservation-idempotency/task_01.md new file mode 100644 index 00000000..19d6ddde --- /dev/null +++ b/docs/specs/0049-baseline-preservation-idempotency/task_01.md @@ -0,0 +1,129 @@ +--- +task: task_01 +spec: 0049-baseline-preservation-idempotency +status: completed +type: backend +complexity: high +--- + +# Task 01: Make Baseline Preservation converge after semantic redistribution + +## Overview + +Deliver the complete Preservation correction from source inventory through +portable Plan assembly. The observable result is a first approved semantic +redistribution that removes consumed source bytes from the live `AGENTS.md`, +followed by empty unchanged reruns and the same convergence after a later +unmarked addition. + +## Requirements + +1. MUST classify only new nonblank unmarked root bytes, excluding + setup-managed blocks and already retained repository-owned rule carriers. +2. MUST back up the complete pre-migration root carrier once for each distinct + content identity before consuming any classified root byte. +3. MUST rebuild the live `AGENTS.md` from active owned regions after every + source entry has a valid approved disposition. +4. MUST preserve exact disposition, retention, preimage, postimage, rollback, + recovery, and backup validation. +5. MUST make an unchanged compatible Preservation produce zero file changes + and no new backup. +6. MUST repeat the same review, backup, consumption, and convergence when a + maintainer later adds new unmarked root bytes. +7. MUST keep public command, Plan, Result, confirmation, and exit-code schemas + unchanged. + +## Subtasks + +- [x] Add failing inventory and end-to-end Plan tests for the reproduced + duplicate-classification and second-backup behavior. +- [x] Separate complete root backup identity from unmarked classification + entries. +- [x] Derive consumed root paths only after complete disposition validation + or verified prior-apply evidence. +- [x] Rebuild consumed `AGENTS.md` postimages from active owned regions. +- [x] Recognize the prior buggy-apply state from a compatible Manifest and + verified content-addressed backup without reclassifying its stale root + payload. +- [x] Avoid duplicating source bytes already present in their approved + semantic or residual owner. +- [x] Cover a later unmarked addition and a third zero-change Plan. +- [x] Preserve existing documentation claims with a contract assertion where + applicable. +- [x] Run focused tests and the complete repository Verification. + +## Acceptance Criteria + +- [x] The first approved Preservation stores the exact original `AGENTS.md` in + `AGENTS..md`. +- [x] Every consumed source byte is absent from the live root and present in + exactly its approved semantic or residual owner. +- [x] Repairing a prior buggy apply does not duplicate source bytes already + present in their approved owner. +- [x] If a prior backed-up payload and a new unmarked addition coexist, only + the new addition enters the Source Baseline. +- [x] Setup-managed blocks and already retained repository-owned rules do not + appear as new Source Baseline Entries. +- [x] A fresh unchanged Preservation has no backup entry and no changed + postimage. +- [x] Adding one new unmarked rule presents only that new source for + classification and produces a backup for the new complete root identity. +- [x] Applying the later migration removes the new source from the live root, + retains its approved owner, and makes the next Plan empty. +- [x] Missing dispositions, stale bytes, malformed markers, backup collisions, + or invalid owners still fail before mutation. +- [x] Public schemas and existing rollback and recovery tests remain + unchanged and pass. + +## Context + +- instruction: `docs/agents/agent-instructions.md` +- instruction: `docs/agents/cli.md` +- instruction: `docs/agents/go.md` +- interface: `internal/baseline/preservation.go` +- interface: `internal/baseline/plan.go` +- interface: `internal/baseline/apply.go` +- interface: `internal/baseline/preservation_test.go` +- interface: `internal/baseline/plan_test.go` +- interface: `internal/baseline/apply_test.go` + +## Verification + +- `rtk go test -count=1 ./internal/baseline -run 'TestPreservationSemanticRedistributionConverges|TestPreservationLaterUnmarkedAdditionConverges|TestPreservationPreviouslyBackedUpRootGuidanceIsNotReclassified|TestPreservationPreviouslyBackedUpRootGuidanceExposesOnlyLaterAddition|TestRootPreservation'` — expected: root inventory, first migration, backed-up migration repair, later addition, backup, and empty-rerun contracts pass. +- `rtk go test -count=1 ./internal/baseline` — expected: every Baseline unit and integration test passes. +- `rtk go test -count=1 ./...` — expected: all repository tests pass. +- `rtk go build -buildvcs=false ./...` — expected: all Roundfix packages build. + +## References + +- `_prd.md` → Goals; User Stories 1–3; Core Features 1–5. +- `_techspec.md` → System Architecture; Interfaces; Testing Approach; Build + Order 1–4. +- ADR-0058; ADR-0064; ADR-0070; ADR-0071; ADR-0073; ADR-0074; ADR-0078. + +## Result + +Completed on 2026-07-26. + +- Preservation now classifies only new nonblank unmarked bytes while complete + carrier bytes continue to determine immutable backup identity. +- Approved root sources are removed from the live `AGENTS.md`; prior buggy + applies are repaired from compatible Manifest plus verified backup evidence + without reclassifying or duplicating their retained rules. +- A later unmarked addition remains independently reviewable, and unchanged + reruns converge to an empty Change Plan. +- The live repository dogfood applied Plan Digest + `sha256:0cffe761a6f33de76ca6dcc92865efcf75e1e65b0c2f5fba7d5a0069b5cce41f` + with only the new backup and `AGENTS.md` update. The next Preservation used + Plan Digest + `sha256:93818af95d2c3c120699d790ea7c35b5c80c1be2dcf068d8262c9ab683cc86b1` + and reported no file changes. + +Verification evidence: + +- Focused Preservation tests: 6 passed. +- `rtk go test -count=1 ./internal/baseline`: 419 passed. +- `rtk go test -count=1 ./...`: 2,369 passed in 22 packages. +- `rtk go build -buildvcs=false ./...`: passed. +- `rtk make verify`: passed, including 2,369 repository tests, 4 skill contract + tests, `roundfix skills check`, and the final binary build. diff --git a/internal/agent/acpx_runner_test.go b/internal/agent/acpx_runner_test.go index 5567e3db..a6667c55 100644 --- a/internal/agent/acpx_runner_test.go +++ b/internal/agent/acpx_runner_test.go @@ -41,6 +41,7 @@ const ( fakeACPXBlockCmd = "ROUNDFIX_FAKE_ACPX_BLOCK_COMMAND" fakeACPXExitCancel = "ROUNDFIX_FAKE_ACPX_EXIT_AFTER_CANCEL" fakeACPXCodexPath = "ROUNDFIX_FAKE_ACPX_CODEX_PATH" + fakeACPXThoughtLen = "ROUNDFIX_FAKE_ACPX_THOUGHT_LENGTH" ) func TestMain(m *testing.M) { @@ -3552,6 +3553,12 @@ func runFakeACPXProcess() int { stdoutByCommand := fakeACPXStringMap(os.Getenv(fakeACPXStdoutBy)) stdoutByCall := fakeACPXStringMap(os.Getenv(fakeACPXStdoutCall)) stderrByCommand := fakeACPXStringMap(os.Getenv(fakeACPXStderrBy)) + if commandKey == "prompt" { + if err := writeFakeACPXThoughtStream(os.Getenv(fakeACPXThoughtLen)); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "write thought stream: %v\n", err) + return 2 + } + } _, _ = io.WriteString(os.Stdout, firstFakeACPXString(stdoutByCall[fakeACPXCallKey(args)], stdoutByCommand[commandKey], fakeSelectionStateOutput(args, os.Getenv(fakeACPXInvokes)), os.Getenv(fakeACPXStdout))) _, _ = io.WriteString(os.Stderr, firstFakeACPXString(stderrByCommand[commandKey], os.Getenv(fakeACPXStderr))) exitByCommand := fakeACPXIntMap(os.Getenv(fakeACPXExitBy)) @@ -3573,6 +3580,28 @@ func runFakeACPXProcess() int { return 0 } +func writeFakeACPXThoughtStream(rawLength string) error { + if rawLength == "" { + return nil + } + length, err := strconv.Atoi(rawLength) + if err != nil { + return fmt.Errorf("parse thought length: %w", err) + } + const chunkSize = 1024 + for remaining := length; remaining > 0; { + size := min(remaining, chunkSize) + update := `{"sessionId":"sealed","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"` + + strings.Repeat("x", size) + + `"}}}` + if _, err := io.WriteString(os.Stdout, acpxUpdateLine(update)); err != nil { + return err + } + remaining -= size + } + return nil +} + func appendFakeACPXInvocation(path string, args []string) error { file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { diff --git a/internal/agent/sealed.go b/internal/agent/sealed.go index 6612cafb..1d1efa67 100644 --- a/internal/agent/sealed.go +++ b/internal/agent/sealed.go @@ -1,7 +1,6 @@ package agent import ( - "bufio" "bytes" "context" "crypto/rand" @@ -19,7 +18,7 @@ import ( const ( SealedPromptMaxInputBytes = 2 << 20 SealedPromptMaxOutputBytes = 512 << 10 - SealedPromptTimeout = 2 * time.Minute + SealedPromptTimeout = 5 * time.Minute sealedACPStreamMaxBytes = 2 << 20 sealedSessionPrefix = "roundfix-baseline-" @@ -289,7 +288,7 @@ func (runner *ACPXRunner) runSealedPromptCommand( command.Env = acpxCommandEnv(codexEnv) command.Dir = request.WorkDir command.Stdin = bytes.NewReader(request.Input) - stdout := newSealedCapture(sealedACPStreamMaxBytes) + stdout := newSealedStreamParser() stderr := newSealedCapture(infrastructureStderrTailBytes) command.Stdout = stdout command.Stderr = stderr @@ -297,9 +296,6 @@ func (runner *ACPXRunner) runSealedPromptCommand( if ctxErr := ctx.Err(); ctxErr != nil { return SealedPromptResult{}, ctxErr } - if errors.Is(err, ErrSealedOutputTooLarge) || stdout.overflow { - return SealedPromptResult{}, ErrSealedOutputTooLarge - } if exitCode, ok := commandExitCode(err); ok { return SealedPromptResult{}, &InfrastructureError{ ExitCode: exitCode, @@ -309,92 +305,160 @@ func (runner *ACPXRunner) runSealedPromptCommand( } return SealedPromptResult{}, fmt.Errorf("run sealed acpx prompt: %w", err) } - if stdout.overflow { - return SealedPromptResult{}, ErrSealedOutputTooLarge - } - return parseSealedPromptStream(stdout.Bytes()) + return stdout.Result() } func parseSealedPromptStream(stream []byte) (SealedPromptResult, error) { - var output bytes.Buffer - var stopReason string - sawResult := false - scanner := bufio.NewScanner(bytes.NewReader(stream)) - scanner.Buffer(make([]byte, 64<<10), sealedACPStreamMaxBytes) - for scanner.Scan() { - line := append([]byte(nil), scanner.Bytes()...) - var message acpxJSONRPCMessage - if err := json.Unmarshal(line, &message); err != nil { - return SealedPromptResult{}, fmt.Errorf("parse sealed acpx JSON-RPC line: %w", err) - } - if message.Error != nil { - return SealedPromptResult{}, fmt.Errorf( - "sealed acpx JSON-RPC error %d: %s", - message.Error.Code, - message.Error.Message, - ) - } - if message.Method == acpMethodSessionUpdate { - update, ok, err := streamUpdateFromSessionUpdatePayload(line) - if err != nil { - return SealedPromptResult{}, err - } - if !ok { - continue - } - switch update.Kind { - case StreamUpdateMessage: - if output.Len()+len(update.Text) > SealedPromptMaxOutputBytes { - return SealedPromptResult{}, ErrSealedOutputTooLarge - } - _, _ = output.WriteString(update.Text) - case StreamUpdateThought: - // Thought remains ephemeral and outside the proposal. - case StreamUpdateToolStarted, StreamUpdateToolUpdated: - return SealedPromptResult{ToolUsed: true}, ErrSealedToolUse - default: - return SealedPromptResult{}, fmt.Errorf( - "sealed ACP attempt emitted unsupported update %q", - update.Kind, - ) - } - continue - } - if len(message.Result) == 0 { - continue - } - var response struct { - StopReason string `json:"stopReason"` + parser := newSealedStreamParser() + _, _ = parser.Write(stream) + return parser.Result() +} + +type sealedStreamParser struct { + line []byte + output bytes.Buffer + stopReason string + sawResult bool + toolUsed bool + err error +} + +func newSealedStreamParser() *sealedStreamParser { + return &sealedStreamParser{ + line: make([]byte, 0, 64<<10), + } +} + +// Write consumes ACPX JSON-RPC output incrementally. Thought updates are +// validated and discarded immediately, so reasoning volume cannot consume the +// bounded proposal buffer. +func (parser *sealedStreamParser) Write(data []byte) (int, error) { + written := len(data) + if parser.err != nil { + return written, nil + } + for len(data) > 0 { + lineEnd := bytes.IndexByte(data, '\n') + if lineEnd < 0 { + parser.appendLine(data) + break } - if err := json.Unmarshal(message.Result, &response); err != nil { - return SealedPromptResult{}, fmt.Errorf( - "parse sealed acpx prompt response: %w", - err, - ) + parser.appendLine(data[:lineEnd]) + if parser.err == nil { + parser.consumeLine(parser.line) } - if sawResult { - return SealedPromptResult{}, errors.New( - "sealed acpx prompt returned more than one terminal result", - ) + parser.line = parser.line[:0] + data = data[lineEnd+1:] + } + return written, nil +} + +func (parser *sealedStreamParser) appendLine(data []byte) { + if parser.err != nil { + return + } + if len(parser.line)+len(data) > sealedACPStreamMaxBytes { + parser.err = ErrSealedOutputTooLarge + parser.line = parser.line[:0] + return + } + parser.line = append(parser.line, data...) +} + +func (parser *sealedStreamParser) consumeLine(line []byte) { + var message acpxJSONRPCMessage + if err := json.Unmarshal(line, &message); err != nil { + parser.err = fmt.Errorf("parse sealed acpx JSON-RPC line: %w", err) + return + } + if message.Error != nil { + parser.err = fmt.Errorf( + "sealed acpx JSON-RPC error %d: %s", + message.Error.Code, + message.Error.Message, + ) + return + } + if message.Method == acpMethodSessionUpdate { + parser.consumeSessionUpdate(line) + return + } + if len(message.Result) == 0 { + return + } + var response struct { + StopReason string `json:"stopReason"` + } + if err := json.Unmarshal(message.Result, &response); err != nil { + parser.err = fmt.Errorf("parse sealed acpx prompt response: %w", err) + return + } + if strings.TrimSpace(response.StopReason) == "" { + return + } + if parser.sawResult { + parser.err = errors.New( + "sealed acpx prompt returned more than one terminal result", + ) + return + } + parser.sawResult = true + parser.stopReason = response.StopReason +} + +func (parser *sealedStreamParser) consumeSessionUpdate(line []byte) { + update, ok, err := streamUpdateFromSessionUpdatePayload(line) + if err != nil { + parser.err = err + return + } + if !ok { + return + } + switch update.Kind { + case StreamUpdateMessage: + if parser.output.Len()+len(update.Text) > SealedPromptMaxOutputBytes { + parser.err = ErrSealedOutputTooLarge + return } - sawResult = true - stopReason = response.StopReason + _, _ = parser.output.WriteString(update.Text) + case StreamUpdateThought: + // Thought remains ephemeral and outside the proposal. + case StreamUpdateToolStarted, StreamUpdateToolUpdated: + parser.toolUsed = true + parser.err = ErrSealedToolUse + default: + parser.err = fmt.Errorf( + "sealed ACP attempt emitted unsupported update %q", + update.Kind, + ) } - if err := scanner.Err(); err != nil { - return SealedPromptResult{}, fmt.Errorf("read sealed acpx output: %w", err) +} + +func (parser *sealedStreamParser) Result() (SealedPromptResult, error) { + if parser.err == nil && len(parser.line) > 0 { + parser.consumeLine(parser.line) + parser.line = parser.line[:0] + } + result := SealedPromptResult{ + Output: append([]byte(nil), parser.output.Bytes()...), + ToolUsed: parser.toolUsed, } - if !sawResult || strings.TrimSpace(stopReason) == "" { + if parser.err != nil { + return result, parser.err + } + if !parser.sawResult || strings.TrimSpace(parser.stopReason) == "" { return SealedPromptResult{}, errors.New( "sealed acpx prompt omitted its terminal result", ) } - if stopReason != "end_turn" { + if parser.stopReason != "end_turn" { return SealedPromptResult{}, fmt.Errorf( "sealed acpx prompt stopped with reason %q", - stopReason, + parser.stopReason, ) } - return SealedPromptResult{Output: append([]byte(nil), output.Bytes()...)}, nil + return result, nil } type sealedCapture struct { diff --git a/internal/agent/sealed_test.go b/internal/agent/sealed_test.go index 78ccd5c6..a79add7e 100644 --- a/internal/agent/sealed_test.go +++ b/internal/agent/sealed_test.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "reflect" + "strconv" "testing" ) @@ -45,6 +46,50 @@ func TestSealedACPXPromptReturnsMessageAndClosesSession(t *testing.T) { assertLastInvocationClosesDisposable(t, harness) } +func TestSealedPromptStreamIgnoresNonTerminalJSONRPCResults(t *testing.T) { + stream := `{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":1}}` + "\n" + + `{"jsonrpc":"2.0","id":2,"result":{"sessionId":"sealed"}}` + "\n" + + acpxUpdateLine( + `{"sessionId":"sealed","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"{\"ok\":true}"}}}`, + ) + + acpxPromptResponseLine("end_turn") + + result, err := parseSealedPromptStream([]byte(stream)) + if err != nil { + t.Fatalf("parse sealed stream with setup results: %v", err) + } + if string(result.Output) != `{"ok":true}` || result.ToolUsed { + t.Fatalf("unexpected sealed result: %+v", result) + } +} + +func TestSealedACPXPromptDiscardsLargeThoughtStreamIncrementally(t *testing.T) { + harness := newFakeACPXHarness(t) + workDir := newPrivateEmptyDirectory(t) + t.Setenv(fakeACPXStdoutCall, sealedSelectionFixtures(t, "gpt-5.5", "xhigh")) + t.Setenv(fakeACPXThoughtLen, strconv.Itoa(sealedACPStreamMaxBytes+1)) + t.Setenv(fakeACPXStdoutBy, mustJSONForTest(t, map[string]string{ + "prompt": acpxUpdateLine( + `{"sessionId":"sealed","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"{\"ok\":true}"}}}`, + ) + acpxPromptResponseLine("end_turn"), + })) + + result, err := harness.runner.RunSealedPrompt(context.Background(), SealedPromptRequest{ + Runtime: RuntimeSpec{ + ID: "codex", Protocol: ProtocolACP, Model: "gpt-5.5", ReasoningEffort: "xhigh", + }, + WorkDir: workDir, + Input: []byte(`{"sealed":true}`), + }) + if err != nil { + t.Fatalf("run sealed prompt with large thought stream: %v", err) + } + if string(result.Output) != `{"ok":true}` || result.ToolUsed { + t.Fatalf("unexpected sealed result: %+v", result) + } + assertLastInvocationClosesDisposable(t, harness) +} + func TestSealedACPXPromptRejectsToolUseAndClosesSession(t *testing.T) { harness := newFakeACPXHarness(t) workDir := newPrivateEmptyDirectory(t) diff --git a/internal/baseline/assets/setups/go-cli.json b/internal/baseline/assets/setups/go-cli.json index 5ea5d710..6f67cbde 100644 --- a/internal/baseline/assets/setups/go-cli.json +++ b/internal/baseline/assets/setups/go-cli.json @@ -5,10 +5,10 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "setups/go-cli.txt" }, - "digest": "51c5fa027bcf6c59023c988b17dfb6bd2a48828583d340e8f591cf4e6f864076", + "digest": "a63d4f17fb66a345d48ae64e8f199d28f52bf7727e92e47c0a3bbf80fae967cc", "skills": [ { "name": "knowledge-workspace", @@ -16,7 +16,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/00-setup/knowledge-workspace" }, "treeDigest": "9571bdfa23f46ec75bb0c820deaee3d3f5513ab203911f3e72dbae4b5760d346" @@ -54,7 +54,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/context7" }, "treeDigest": "00ac7891ecaa432562712a2d06b728d37f30b11bbe839fe5e264af7da425180a" @@ -68,13 +68,24 @@ }, "contentDigest": "5163a850925247beeec16475311d1214e7c89258d8d5924ff4a771fb9a6ec079" }, + { + "name": "exa-web-search", + "path": "skills/01-discovery/exa-web-search", + "source": { + "type": "github", + "repository": "marcioaltoe/skills", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", + "path": "skills/01-discovery/exa-web-search" + }, + "treeDigest": "9e9f47943ae5d0bf3438013d31e2d02610fd3b8d4b40cf6bd2db17a695b3e7ae" + }, { "name": "grill-with-docs", "path": "skills/01-discovery/grill-with-docs", "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/grill-with-docs" }, "treeDigest": "717f9874c97277f88ed97d723b62a95e2584e195e596bdf6cd7a17ddd6103e80" @@ -85,7 +96,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/grilling" }, "treeDigest": "1649e3036843d33d6ca0f9aab79269c456e4ae13e14754422c60920eb3cdc0ac" @@ -96,7 +107,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/the-fool" }, "treeDigest": "1b4f41804f0c288993b0a5a295d90d538d7dd1bf7d6971f8af13d7ee47cfabb1" @@ -125,7 +136,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/agentic-cli-design" }, "treeDigest": "d261b7713dc1baa3136895904659bffffededfb9926d94f7c7ba030608a4ff79" @@ -136,7 +147,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/domain-modeling" }, "treeDigest": "df407341e313fde8323571b80d2da11cd35d58a362eb5f792edd67ccec30c814" @@ -147,7 +158,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/tui-design" }, "treeDigest": "bca89eb52289063d58aa1d9fd5d2346619c23fa182932ea51d221f513949e6b6" @@ -176,7 +187,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/bubbletea" }, "treeDigest": "78c2ec366ce04c37d9de64b11e2fe73d75cd374d8e8be6c41eb2381a27351de1" @@ -187,7 +198,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/coding-guidelines" }, "treeDigest": "1ed1e711b511f8ea14e0e4f83949d3395e7b202e8344c52e586256c7959ce0a8" @@ -198,7 +209,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/golang-cli" }, "treeDigest": "f28f13c594497f2ae706d64924ef930291d82b3a6061ac9eea57560ba53e91ae" @@ -209,7 +220,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/golang-concurrency" }, "treeDigest": "ce957e711e22faf2a367bfc08f567b4dac47c2d9afd368f501a24593ca32c456" @@ -220,7 +231,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/golang-context" }, "treeDigest": "1d843312ec1aefc626a67b4f75cfa4f2b74056d00233e24e251401c1893ed22b" @@ -231,7 +242,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/golang-error-handling" }, "treeDigest": "4a2818a6d10ed0a0ddd38451d68c55f5707ec94bcbe5d4a7d80d05cfc16257a1" @@ -242,7 +253,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/golang-lint" }, "treeDigest": "1754a1db0ba3f537438fa7c1b46267f1ea208282a633bf08a880ea018bac859e" @@ -253,7 +264,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/golang-testing" }, "treeDigest": "9c5288a956ff62f55da8ec527e203ccea9590d54487b1f7e04a471a1de3ba171" @@ -282,7 +293,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/no-workarounds" }, "treeDigest": "f5b17142f32629700a4f62e1b2cdf2e4ed762aee10cf9105bf9b064a28aa1337" @@ -293,7 +304,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/testing-boss" }, "treeDigest": "2dd385923c50384455d0722429051b99fd97d9351fc60a0e58191a3bcf982fe6" @@ -322,7 +333,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/review" }, "treeDigest": "5d27c3aeaed9505e21ee22e03367f2874db2969699dd5cfca621b2981b524174" @@ -333,7 +344,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/systematic-debugging" }, "treeDigest": "12f5126e8e9bb86c75c58e87aff0e1f08c7143ffe47b1a7694414fa41ddaaba6" @@ -353,7 +364,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/conventional-commits" }, "treeDigest": "ef7a6718684adba3919e3211d470d95351f5e4949e9d3a9b290e4f2c77a8739c" @@ -364,7 +375,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/github-pr-workflow" }, "treeDigest": "082906ba30c577d2ddb4fda1ef0c75bea42fb1ff139c898e377d7f9caba57d4a" @@ -375,7 +386,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/handoff" }, "treeDigest": "75b23d67b04e129fb536193e033b1fc8c3caa99f42652077556f051e92f75cfa" @@ -386,7 +397,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/tech-writer" }, "treeDigest": "18efb1105eebbd46e051d643830f1758f089cdf0f0a912a8be5d79fc657fd606" diff --git a/internal/baseline/assets/setups/rust-cli.json b/internal/baseline/assets/setups/rust-cli.json index 2ef095a5..e3c2353c 100644 --- a/internal/baseline/assets/setups/rust-cli.json +++ b/internal/baseline/assets/setups/rust-cli.json @@ -5,10 +5,10 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "setups/rust-cli.txt" }, - "digest": "942d56217031103f7be75cf9c59db72bbf19af0cd13dae8aeb5e62531ed6350b", + "digest": "cdd07e328d08b7e24708f499cb7e40884b994a72df046ea64e73e1d078a85c05", "skills": [ { "name": "knowledge-workspace", @@ -16,7 +16,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/00-setup/knowledge-workspace" }, "treeDigest": "9571bdfa23f46ec75bb0c820deaee3d3f5513ab203911f3e72dbae4b5760d346" @@ -54,7 +54,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/context7" }, "treeDigest": "00ac7891ecaa432562712a2d06b728d37f30b11bbe839fe5e264af7da425180a" @@ -68,13 +68,24 @@ }, "contentDigest": "5163a850925247beeec16475311d1214e7c89258d8d5924ff4a771fb9a6ec079" }, + { + "name": "exa-web-search", + "path": "skills/01-discovery/exa-web-search", + "source": { + "type": "github", + "repository": "marcioaltoe/skills", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", + "path": "skills/01-discovery/exa-web-search" + }, + "treeDigest": "9e9f47943ae5d0bf3438013d31e2d02610fd3b8d4b40cf6bd2db17a695b3e7ae" + }, { "name": "grill-with-docs", "path": "skills/01-discovery/grill-with-docs", "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/grill-with-docs" }, "treeDigest": "717f9874c97277f88ed97d723b62a95e2584e195e596bdf6cd7a17ddd6103e80" @@ -85,7 +96,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/grilling" }, "treeDigest": "1649e3036843d33d6ca0f9aab79269c456e4ae13e14754422c60920eb3cdc0ac" @@ -96,7 +107,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/the-fool" }, "treeDigest": "1b4f41804f0c288993b0a5a295d90d538d7dd1bf7d6971f8af13d7ee47cfabb1" @@ -125,7 +136,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/agentic-cli-design" }, "treeDigest": "d261b7713dc1baa3136895904659bffffededfb9926d94f7c7ba030608a4ff79" @@ -136,7 +147,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/domain-modeling" }, "treeDigest": "df407341e313fde8323571b80d2da11cd35d58a362eb5f792edd67ccec30c814" @@ -165,7 +176,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/clap-rust" }, "treeDigest": "da206d7c6ea85dba8b2d53479a5bf0ac0347d500c599c3fefedfa1d2f8e68ff0" @@ -176,7 +187,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/coding-guidelines" }, "treeDigest": "1ed1e711b511f8ea14e0e4f83949d3395e7b202e8344c52e586256c7959ce0a8" @@ -205,7 +216,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/no-workarounds" }, "treeDigest": "f5b17142f32629700a4f62e1b2cdf2e4ed762aee10cf9105bf9b064a28aa1337" @@ -216,7 +227,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/rust" }, "treeDigest": "f5f106e3c2eeb4a5f2025f4b87b28463ccffb1e68e1cbebaa55a2a6ac3c83360" @@ -227,7 +238,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/rust-cli" }, "treeDigest": "67e04140a6cf5e6de0aa2a9a7f7423971b02c7953716e331ce23fb85c20da3b2" @@ -238,7 +249,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/testing-boss" }, "treeDigest": "2dd385923c50384455d0722429051b99fd97d9351fc60a0e58191a3bcf982fe6" @@ -267,7 +278,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/review" }, "treeDigest": "5d27c3aeaed9505e21ee22e03367f2874db2969699dd5cfca621b2981b524174" @@ -278,7 +289,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/systematic-debugging" }, "treeDigest": "12f5126e8e9bb86c75c58e87aff0e1f08c7143ffe47b1a7694414fa41ddaaba6" @@ -298,7 +309,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/conventional-commits" }, "treeDigest": "ef7a6718684adba3919e3211d470d95351f5e4949e9d3a9b290e4f2c77a8739c" @@ -309,7 +320,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/github-pr-workflow" }, "treeDigest": "082906ba30c577d2ddb4fda1ef0c75bea42fb1ff139c898e377d7f9caba57d4a" @@ -320,7 +331,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/handoff" }, "treeDigest": "75b23d67b04e129fb536193e033b1fc8c3caa99f42652077556f051e92f75cfa" @@ -331,7 +342,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/tech-writer" }, "treeDigest": "18efb1105eebbd46e051d643830f1758f089cdf0f0a912a8be5d79fc657fd606" @@ -342,7 +353,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/08-release/cut-release" }, "treeDigest": "0320252020ae5b3fdcb4ccff9b122f1f2366c8b2e4ff7e0aab64e8b665300825" diff --git a/internal/baseline/assets/setups/typescript-bun.json b/internal/baseline/assets/setups/typescript-bun.json index b432bbf1..2a05496d 100644 --- a/internal/baseline/assets/setups/typescript-bun.json +++ b/internal/baseline/assets/setups/typescript-bun.json @@ -5,10 +5,10 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "setups/typescript-bun.txt" }, - "digest": "2b1b1c6b74b61ee804e21c21d02b95f279f2046702d3235d359c50808047aea8", + "digest": "b23d5f4dddee60153593ff4f7ed927c93ca30f956a4725d9eb413cd507f0c6b2", "skills": [ { "name": "find-rules", @@ -16,7 +16,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/00-setup/find-rules" }, "treeDigest": "195174052c5f5e3fe893911273287736bb2eb87c0376b90ddaa8dae39d37cda2" @@ -27,7 +27,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/00-setup/find-skills" }, "treeDigest": "745273731a8d7e9e8405634bf8de030481931745913d32ecc23d8a59f4b675dc" @@ -38,7 +38,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/00-setup/knowledge-workspace" }, "treeDigest": "9571bdfa23f46ec75bb0c820deaee3d3f5513ab203911f3e72dbae4b5760d346" @@ -58,7 +58,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/00-setup/turborepo" }, "treeDigest": "c75f567a94def9f877f9677138d017907a24eb59fbb6ae513ded4befd645ca10" @@ -69,7 +69,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/context7" }, "treeDigest": "00ac7891ecaa432562712a2d06b728d37f30b11bbe839fe5e264af7da425180a" @@ -80,7 +80,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/grill-with-docs" }, "treeDigest": "717f9874c97277f88ed97d723b62a95e2584e195e596bdf6cd7a17ddd6103e80" @@ -91,7 +91,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/grilling" }, "treeDigest": "1649e3036843d33d6ca0f9aab79269c456e4ae13e14754422c60920eb3cdc0ac" @@ -111,7 +111,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/architectural-analysis" }, "treeDigest": "cc404d7aeb0b2103ab81490e34f575553b34b3970b7528cd3ad5da9d8acb8e5a" @@ -122,7 +122,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/backend-docs" }, "treeDigest": "8f15a67e96536b3d0162e420275c27ecdfae149c7d230a88238204dd3c004e47" @@ -133,7 +133,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/clean-architecture" }, "treeDigest": "44ee1b15595241a9880825388db924176537a2b5da7b9c4feee842d006c6686e" @@ -144,7 +144,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/codebase-design" }, "treeDigest": "d0c958f35d77a019f4d63466c218cf73f98fc7f26844bdea53668d7a12f05585" @@ -155,7 +155,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/data-sync-workflows" }, "treeDigest": "6cbfcb860958e36b6d3acab150c854967cf348c71080216bdcffb8e42b01cc0a" @@ -166,7 +166,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/design-patterns" }, "treeDigest": "19102072c8d4229d2a396a112214463720b97655633938db094f1da80372272b" @@ -177,7 +177,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/domain-modeling" }, "treeDigest": "df407341e313fde8323571b80d2da11cd35d58a362eb5f792edd67ccec30c814" @@ -188,7 +188,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/frontend-docs" }, "treeDigest": "e9c8c512573f0b07c19fa54eb67e8938a6dc4dd580ba2a6fcfd4b4e74e0563a4" @@ -199,7 +199,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/security-best-practices" }, "treeDigest": "046ac4e5fe96d34686d72a554b7ee80090e7d894237b119db9696545b2014bd4" @@ -210,7 +210,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/security-threat-model" }, "treeDigest": "c26849057133bed892c607f7874d14630432b707c2b2ec00bdca8dc08a53e5d4" @@ -221,7 +221,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/tactical-ddd" }, "treeDigest": "ce5d09c33f4d09f08032be785405a6b7ed3f1b2bfdf40e5eb12ab4f5ada6653f" @@ -232,7 +232,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/ui-ux-pro-max" }, "treeDigest": "71f125f5b04dc1473fde612c7526f33696fea6ebcc09c98be77423fd038e5efa" @@ -243,7 +243,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/vertical-slice-architecture" }, "treeDigest": "00e21d9bd70c4e673f8fdc8a6db5cc4f347f0a5fab23d24e7a48b90251fc7b0b" @@ -272,7 +272,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/ai-sdk" }, "treeDigest": "3f0cc0187836c471fa3de03765ec52019567bc0a7bbe6ef11eb4812b85872ea1" @@ -283,7 +283,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/baseline-ui" }, "treeDigest": "f2967ee7bd537104069cd0ba3bde752c81be7506a0a850eeccd6953d1b0a61ef" @@ -294,7 +294,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/better-auth" }, "treeDigest": "cc01a4be5e3caa50be0deb8fedf27194d206033332712fcf3a7ef42c5263e913" @@ -305,7 +305,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/bun" }, "treeDigest": "6ffb084aac32b67b050c9e54170da97c5752f4b0ca4099b84b8d66040bf63fc2" @@ -316,7 +316,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/building-components" }, "treeDigest": "dc12496b234128deef50d3c5a48a11fe91f1e77532bb2f1ccdac691cb5383bd1" @@ -327,7 +327,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/coding-guidelines" }, "treeDigest": "1ed1e711b511f8ea14e0e4f83949d3395e7b202e8344c52e586256c7959ce0a8" @@ -338,7 +338,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/drizzle-orm" }, "treeDigest": "c393fe752af2901b6ce9c1f71d9e0b7c930dfed9f31f6b4d3b66715707545d66" @@ -349,7 +349,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/external-api-adapters" }, "treeDigest": "c02f3627de69a49db8cb96388eb2bda89971b33fa6f2a94791aa8a53db9d6439" @@ -360,7 +360,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/frontend-design" }, "treeDigest": "64f90f1e3bd0c7fb8a65e2be24357f6992e4b11e62e2f86f0edad8ac73105f31" @@ -371,7 +371,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/hono" }, "treeDigest": "3d456a22ff5e50238a88f3f42172047355f3ce1a531e895b2cb966294389db50" @@ -382,7 +382,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/hono-api-best-practices" }, "treeDigest": "7abc04034c9d710a45765e56425524ab3a7de82bce82b28e9e512de1e984ffa8" @@ -411,7 +411,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/inngest" }, "treeDigest": "c4f41c88ee8698d969353dfc0e41686df83eeeabff67f3ef97dda89562664742" @@ -422,7 +422,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/integration-contract-testing" }, "treeDigest": "adf2bfe3e4421155cc10f2eeffada5b0d23745b9d8ee4f9d618a19f74602e459" @@ -433,7 +433,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/interface-design" }, "treeDigest": "b2f98e94a96d43e1225bb2eacd426c6417ad8919fb2d2bcbe48102381f9cb1e0" @@ -444,7 +444,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/interaction-design" }, "treeDigest": "a496658d82d96f379b16c89449aac72dafe6f639e161a0477d0b7bda07842622" @@ -455,7 +455,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/logtape" }, "treeDigest": "67145ccaa2c5606daec89ba65ebf9f8b82c782fa5200392b3fb8b83996ea64ca" @@ -466,7 +466,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/motion" }, "treeDigest": "fd4453c6cdc19b7707b31d2ced98756ba5c768d6ade652b043934672dc57f445" @@ -477,7 +477,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/no-workarounds" }, "treeDigest": "f5b17142f32629700a4f62e1b2cdf2e4ed762aee10cf9105bf9b064a28aa1337" @@ -488,7 +488,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/react" }, "treeDigest": "8719a28cfb41e4b5833ddebb6c99f5bcc4c2772ab64e4f1c98cfb1b2caa3b5cb" @@ -499,7 +499,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/react-best-practices" }, "treeDigest": "6919dd896196c0dcaa4c86af5fda7b27be3ae4e6ee6a7dcd87ffd6548efce196" @@ -510,7 +510,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/react-composition-patterns" }, "treeDigest": "ccc2ac65ffe42499946843aba4c5d24a8a4f75597c9850f81cdf3d57c4908375" @@ -521,7 +521,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/react-hook-form-zod" }, "treeDigest": "eb824393e16d915bd4b3829a4d4b7530b358c68634b26446a281cc7854bdb608" @@ -532,10 +532,10 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/shadcn" }, - "treeDigest": "1d41c014e249719d5bb6431b4e1539721c3c68e9e081170dd9e7d906b3323829" + "treeDigest": "c57e3cc8688fe5f0956c8e91ee02d1ee97fb5b0e8115e2d6ca6447c1ade69686" }, { "name": "solid", @@ -543,7 +543,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/solid" }, "treeDigest": "90c11691f139398f7e1f185b3913e4bec0f5c618a2e7cd257bb8dd45bd7bc9e7" @@ -554,7 +554,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/tailwindcss" }, "treeDigest": "5d6bbe69ca2cc42cddd53bc0045a3ce89192886bd795aff1503554551a3f8a6f" @@ -565,7 +565,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/tanstack-query" }, "treeDigest": "68511c4f725ff4f372004ee90123c19f043988f95fe10f170d6dfad658d9581d" @@ -576,7 +576,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/tanstack-router" }, "treeDigest": "08fda8bc11922e9b50eac10c14951a24867d7767b3bdee11580c7b73d54df1fc" @@ -587,7 +587,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/tanstack-table" }, "treeDigest": "b1961df1699a49f726bd5173291fab80dfa010a287d262ee2afae4283beb1f30" @@ -598,7 +598,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/tdd" }, "treeDigest": "38a929d813ead141012377ffc7c95cf7de31462eae973c8c8b81503242cea0c2" @@ -609,7 +609,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/testing-boss" }, "treeDigest": "2dd385923c50384455d0722429051b99fd97d9351fc60a0e58191a3bcf982fe6" @@ -620,7 +620,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/typescript-advanced" }, "treeDigest": "467347ba9875e39c340e7ed0ffe1fe4fa084526e165740bc60846fc25f879d6b" @@ -631,7 +631,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/vite" }, "treeDigest": "ee15ec81c25291a1dcbadf39c908d22d3fbaec55a3c68408241b0c171710f5ac" @@ -642,7 +642,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/vitest" }, "treeDigest": "dc2b2baefb70e0bc1906200e98726d9b9daee5fe80d313f91d3ef9dcee8c9d95" @@ -653,7 +653,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/zod" }, "treeDigest": "14b33bec4de7c14904833a3bae82798a508e7d9f0e7634ebb29a728eb51e4163" @@ -664,7 +664,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/zustand" }, "treeDigest": "ee92be1abf3e2aed698e9c289968815c225ff968bd38b8e6ed370adc98620d80" @@ -675,7 +675,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/agent-browser" }, "treeDigest": "22eab8b7ccce9362a3fb4d31fae366fb5a128de16c37cb7c7fd567fe18e7de64" @@ -686,7 +686,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/clean-code" }, "treeDigest": "75e407a08a435640af90eea9fd11e0f69d384bdd91bedd81693c4c070cf53669" @@ -697,7 +697,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/core-web-vitals" }, "treeDigest": "b341ec953d9b91cfb384c5f210c7e0aadc26aa24341a2bcd55d960dca23b0313" @@ -708,7 +708,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/diagnosing-bugs" }, "treeDigest": "224657becc36cee23345dd919d3cfb9e9c9372c0149cb51ca4cbbe21249eda7d" @@ -719,7 +719,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/fixing-accessibility" }, "treeDigest": "47e553ac1c56afab1390631de5b3362a075136cabcd3494c31e0878ccf327f93" @@ -730,7 +730,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/fixing-metadata" }, "treeDigest": "a0f4df92f1f2001e845c9bb6c3718307c3aaf57c7bdeb422de2baac40a9e67e3" @@ -741,7 +741,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/fixing-motion-performance" }, "treeDigest": "5f023d388394ad72a1f7a09aab28f40bbb7945d920e38b5dd63a94e437219370" @@ -752,7 +752,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/git-rebase" }, "treeDigest": "d705f9197dce287736523a971309c00d5e2d7927a3d987f9ea302ca82911f51a" @@ -763,7 +763,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/observability-audit" }, "treeDigest": "ef3a3dfe9685d75de538bc4570273f8375a2f22ffeeae5da0743a771c199048a" @@ -774,7 +774,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/onioncry" }, "treeDigest": "f918508da464fcc37b436a5a26fc243ffa159ead9e07b19e991451ed0ca53b6c" @@ -794,7 +794,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/review" }, "treeDigest": "5d27c3aeaed9505e21ee22e03367f2874db2969699dd5cfca621b2981b524174" @@ -805,7 +805,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/systematic-debugging" }, "treeDigest": "12f5126e8e9bb86c75c58e87aff0e1f08c7143ffe47b1a7694414fa41ddaaba6" @@ -825,7 +825,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/wcag-audit-patterns" }, "treeDigest": "86ff7df0a30add55040d6accc546edbac156be3c3aa514500dbc5a72a49c4a7e" @@ -836,7 +836,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/web-design-guidelines" }, "treeDigest": "43dd4bccf0d365a2817fb47d7c71ff2297a71495d0ec302a33997adcc5201dba" @@ -856,7 +856,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/conventional-commits" }, "treeDigest": "ef7a6718684adba3919e3211d470d95351f5e4949e9d3a9b290e4f2c77a8739c" @@ -867,7 +867,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/crafting-effective-readmes" }, "treeDigest": "53447c2e46b78d4c49de259d0c848d950470317c7b782af66ff916580a90ddf8" @@ -878,7 +878,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/github-pr-workflow" }, "treeDigest": "082906ba30c577d2ddb4fda1ef0c75bea42fb1ff139c898e377d7f9caba57d4a" @@ -889,7 +889,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/handoff" }, "treeDigest": "75b23d67b04e129fb536193e033b1fc8c3caa99f42652077556f051e92f75cfa" @@ -900,7 +900,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/tech-writer" }, "treeDigest": "18efb1105eebbd46e051d643830f1758f089cdf0f0a912a8be5d79fc657fd606" @@ -911,7 +911,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/08-release/docker" }, "treeDigest": "8b4991d73d61617668168c77676b605e98751e5d8676611ae0c0bb60fb593da9" @@ -931,7 +931,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/10-marketing/business-storyteller" }, "treeDigest": "0d4c5592f9b4c8557c1b93f892b68c25936bc974a17c341b8cd9a871e7fbed3b" @@ -960,7 +960,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/exa-web-search" }, "treeDigest": "9e9f47943ae5d0bf3438013d31e2d02610fd3b8d4b40cf6bd2db17a695b3e7ae" @@ -971,7 +971,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/03-engineering-design/feature-systems-pattern" }, "treeDigest": "ba80214eb04b3aa7e2304f66af69943afa60752e441031318e1e88ce8b35eccc" @@ -982,7 +982,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/firecrawl" }, "treeDigest": "55a933a3c330378d75147f66d0c6481915c53e5d16fb18443502fe311204cc09" @@ -993,7 +993,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/game-changing-features" }, "treeDigest": "17355b3d4babf23fb2c994dda5c895a9f4dae87479709d04dc1ab892db634960" @@ -1004,10 +1004,10 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/humanizer" }, - "treeDigest": "c7c8ed25e0954bb101b2e8160e6cb2d32f0428b1f768d500ac76d2d76d392337" + "treeDigest": "1bf4a9bed34f862bb171fc4296021ed120f27369dc99fb8348cd5dee2306dca5" }, { "name": "refactoring-analysis", @@ -1015,7 +1015,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/06-review-repair/refactoring-analysis" }, "treeDigest": "8d59cb5b43fdeb6294f91419464b39844e7420323ac7a3f11999586d675c04e5" @@ -1035,7 +1035,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/01-discovery/the-fool" }, "treeDigest": "1b4f41804f0c288993b0a5a295d90d538d7dd1bf7d6971f8af13d7ee47cfabb1" @@ -1055,7 +1055,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/04-issue-decomposition/triage" }, "treeDigest": "3c5cc9dc0d4c9610f7ecff92605e37e8581995ec0d3cc622989f21b3e01fca0f" @@ -1066,7 +1066,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/05-implementation-loop/to-prompt" }, "treeDigest": "e41d189ef783d6154ef7eb91e8e161a2f2aea9a8f2ede5bf4e34698c2a0fe6ba" @@ -1077,7 +1077,7 @@ "source": { "type": "github", "repository": "marcioaltoe/skills", - "ref": "236847f6956134bf468abb641bac0493a899bca5", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "skills/07-evidence-delivery/writing-clearly-and-concisely" }, "treeDigest": "afc4647ae8647b242cae327b5fa2f1158273109af08d0fa95c744d2ba68f5ef2" diff --git a/internal/baseline/classification.go b/internal/baseline/classification.go index b11137de..11b98f22 100644 --- a/internal/baseline/classification.go +++ b/internal/baseline/classification.go @@ -10,6 +10,7 @@ import ( "fmt" "sort" "strings" + "unicode/utf8" ) const ( @@ -49,6 +50,14 @@ type ClassificationProposalContract struct { Rules []string `json:"rules"` } +// ClassificationSemanticEntry exposes the exact readable source text used for +// semantic analysis without changing the byte evidence that planning admits. +type ClassificationSemanticEntry struct { + EntryID string `json:"entryId"` + Text string `json:"text"` + Readable bool `json:"readable"` +} + // AnalysisSnapshot is the canonical, checkout-free input to one sealed // semantic classification attempt. type AnalysisSnapshot struct { @@ -58,6 +67,7 @@ type AnalysisSnapshot struct { ProposalContract ClassificationProposalContract `json:"proposalContract"` SourceBaseline ClassificationSource `json:"sourceBaseline"` Entries []ReadoptionSourceEntry `json:"entries"` + SemanticEntries []ClassificationSemanticEntry `json:"semanticEntries"` Destinations []ClassificationDestination `json:"destinations"` SnapshotDigest string `json:"snapshotDigest"` } @@ -124,6 +134,7 @@ func NewAnalysisSnapshot( ProposalContract: classificationProposalContract(), SourceBaseline: ClassificationSource{ID: source.ID, Digest: source.Digest}, Entries: cloneClassificationEntries(source.Entries), + SemanticEntries: classificationSemanticEntries(source.Entries), Destinations: supportedClassificationDestinations(owners), } digest, err := computeAnalysisSnapshotDigest(snapshot) @@ -288,10 +299,16 @@ func normalizeClassificationProposal( ) } seen[disposition.EntryID] = struct{}{} - if err := validateClassificationDisposition(snapshot, entry, disposition); err != nil { + normalizedDisposition := cloneReadoptionDisposition(disposition) + deriveClassificationByteEvidence(entry, &normalizedDisposition) + if err := validateClassificationDisposition( + snapshot, + entry, + normalizedDisposition, + ); err != nil { return ClassificationProposal{}, err } - normalized = append(normalized, cloneReadoptionDisposition(disposition)) + normalized = append(normalized, normalizedDisposition) } for _, entry := range snapshot.Entries { if _, exists := seen[entry.ID]; !exists { @@ -311,6 +328,32 @@ func normalizeClassificationProposal( }, nil } +func deriveClassificationByteEvidence( + entry ReadoptionSourceEntry, + disposition *ReadoptionDisposition, +) { + if disposition.EntryDigest == "" { + disposition.EntryDigest = entry.Digest + } + if disposition.Destination == nil { + return + } + switch disposition.Disposition { + case "repository-rules": + if disposition.Destination.Digest == "" { + disposition.Destination.Digest = entry.Digest + } + if disposition.Destination.ProposedBytes == "" { + disposition.Destination.ProposedBytes = + base64.StdEncoding.EncodeToString(entry.SourceBytes) + } + case "repository-document": + if disposition.Destination.Digest == "" { + disposition.Destination.Digest = entry.Digest + } + } +} + func validateClassificationDisposition( snapshot AnalysisSnapshot, entry ReadoptionSourceEntry, @@ -452,6 +495,12 @@ func validateAnalysisSnapshot(snapshot AnalysisSnapshot) error { AnalysisSnapshotMaxEntries, ) } + if !equalClassificationSemanticEntries( + snapshot.SemanticEntries, + classificationSemanticEntries(snapshot.Entries), + ) { + return errors.New("validate Analysis Snapshot: semantic entries changed") + } if err := validateClassificationDestinations(snapshot.Destinations); err != nil { return err } @@ -566,7 +615,6 @@ func classificationProposalContract() ClassificationProposalContract { }, RequiredDispositionFields: []string{ "entryId", - "entryDigest", "classification", "disposition", "destination", @@ -580,15 +628,49 @@ func classificationProposalContract() ClassificationProposalContract { }, Rules: []string{ "Return exactly one JSON object with no prose.", - "Return exactly one disposition for every entryId and copy its entryDigest.", + "Return exactly one disposition for every entryId.", + "Use semanticEntries text for meaning; entries sourceBytes remain the authoritative byte evidence.", + "Treat readable=false as unavailable semantic text and preserve it conservatively.", + "Do not return entryDigest, destination digest, or proposedBytes; the planner derives exact byte evidence locally.", "Use rejected with a null destination and non-empty reason for non-governed, managed, or empty evidence.", - "Use repository-rules only with the advertised documentType and path, exact source bytes as canonical base64, their digest, and a non-empty reason.", - "Use repository-document only with one advertised active agent-guide path, the exact entry digest, and a non-empty reason.", + "Use repository-rules only with the advertised documentType and path and a non-empty reason.", + "Use repository-document only with one advertised active agent-guide path and a non-empty reason.", "Use managed-entry only with one advertised active managedId and a non-empty reason.", }, } } +func classificationSemanticEntries( + entries []ReadoptionSourceEntry, +) []ClassificationSemanticEntry { + result := make([]ClassificationSemanticEntry, len(entries)) + for index, entry := range entries { + result[index] = ClassificationSemanticEntry{ + EntryID: entry.ID, + Readable: utf8.Valid(entry.SourceBytes), + } + if result[index].Readable { + result[index].Text = string(entry.SourceBytes) + } + } + return result +} + +func equalClassificationSemanticEntries( + first []ClassificationSemanticEntry, + second []ClassificationSemanticEntry, +) bool { + if first == nil || len(first) != len(second) { + return false + } + for index := range first { + if first[index] != second[index] { + return false + } + } + return true +} + func equalClassificationProposalContract( first ClassificationProposalContract, second ClassificationProposalContract, diff --git a/internal/baseline/classification_test.go b/internal/baseline/classification_test.go index eada890a..3359ba12 100644 --- a/internal/baseline/classification_test.go +++ b/internal/baseline/classification_test.go @@ -8,6 +8,7 @@ package baseline import ( "bytes" "context" + "encoding/base64" "encoding/json" "strings" "testing" @@ -57,6 +58,59 @@ func TestSealedClassificationSnapshotIsCanonicalAndBounded(t *testing.T) { } } +func TestClassificationSnapshotMakesExactTextAvailableToSealedAnalysis(t *testing.T) { + snapshot := classificationTestSnapshot(t) + canonical, err := snapshot.CanonicalBytes() + if err != nil { + t.Fatalf("marshal Analysis Snapshot: %v", err) + } + entry := snapshot.Entries[0] + for _, required := range []string{ + `"semanticEntries"`, + `"entryId":"` + entry.ID + `"`, + `"text":"keep this rule\n"`, + } { + if !bytes.Contains(canonical, []byte(required)) { + t.Fatalf("Analysis Snapshot does not expose %q:\n%s", required, canonical) + } + } +} + +func TestClassificationProposalDerivesByteEvidenceLocally(t *testing.T) { + snapshot := classificationTestSnapshot(t) + entry := snapshot.Entries[0] + destination := snapshot.Destinations[1] + proposal := map[string]any{ + "schemaVersion": ClassificationProposalSchemaVersion, + "snapshotDigest": snapshot.SnapshotDigest, + "dispositions": []map[string]any{{ + "entryId": entry.ID, + "classification": "normative-clause", + "disposition": "repository-rules", + "destination": map[string]any{ + "documentType": destination.DocumentType, + "path": destination.Path, + }, + "reason": "No active semantic guide owns this repository policy.", + }}, + } + payload, err := json.Marshal(proposal) + if err != nil { + t.Fatal(err) + } + parsed, err := ParseClassificationProposal(payload, snapshot) + if err != nil { + t.Fatalf("classification without agent-computed byte evidence was rejected: %v", err) + } + got := parsed.Dispositions[0] + if got.EntryDigest != entry.Digest || + got.Destination == nil || + got.Destination.Digest != entry.Digest || + got.Destination.ProposedBytes != base64.StdEncoding.EncodeToString(entry.SourceBytes) { + t.Fatalf("classification byte evidence was not derived locally: %+v", got) + } +} + func TestSemanticRuleDistributionAdmitsOnlyActiveSemanticOwners(t *testing.T) { repo := newPlanRepository(t) catalog, err := LoadEmbeddedCatalog() diff --git a/internal/baseline/plan.go b/internal/baseline/plan.go index fbb9ba33..f8510ee4 100644 --- a/internal/baseline/plan.go +++ b/internal/baseline/plan.go @@ -339,8 +339,7 @@ func BuildPlan(ctx context.Context, request PlanRequest) (PlanOutcome, error) { initial.Root, preservation.RepositoryRulesBytes, decisionBool(decisions, "repository.extension.enabled"), - preservationRequest.Mode == PreservationModePreservation && - preservationRequest.Decisions != nil, + preservationRedistributesRecognizedRepositoryRules(preservation), ) if err != nil { return PlanOutcome{}, err @@ -409,8 +408,7 @@ func BuildPlan(ctx context.Context, request PlanRequest) (PlanOutcome, error) { initial.Root, preservation.RepositoryRulesBytes, decisionBool(decisions, "repository.extension.enabled"), - preservationRequest.Mode == PreservationModePreservation && - preservationRequest.Decisions != nil, + preservationRedistributesRecognizedRepositoryRules(preservation), ) if err != nil { return PlanOutcome{}, err @@ -638,14 +636,7 @@ func planSpecificRepository( selected = proposed selectedPath = "Baseline Readoption" } else if !bytes.Equal(selected, proposed) { - return specificRepositoryPlan{}, []Finding{{ - Code: "baseline.repository-rules.conflict", - Path: specificRepositoryPath, - Message: fmt.Sprintf( - "repository-specific rule carriers conflict: %s and Baseline Readoption contain different bytes", - selectedPath, - ), - }}, nil + selected = appendRepositoryRules(selected, proposed) } } if !enabled { @@ -684,6 +675,35 @@ func planSpecificRepository( return plan, nil, nil } +func preservationRedistributesRecognizedRepositoryRules( + preservation RootPreservationPlan, +) bool { + if preservation.Mode != PreservationModePreservation || + len(preservation.Dispositions) == 0 { + return false + } + for _, entry := range preservation.SourceBaseline.Entries { + switch entry.Path { + case specificRepositoryPath, legacyRepositoryPath, legacyRepositoryRulesPath: + return true + } + } + return false +} + +func appendRepositoryRules(existing, proposed []byte) []byte { + if bytes.Contains(existing, proposed) { + return append([]byte(nil), existing...) + } + combined := append([]byte(nil), existing...) + if len(combined) != 0 && + !bytes.HasSuffix(combined, []byte("\n")) && + !bytes.HasPrefix(proposed, []byte("\n")) { + combined = append(combined, '\n') + } + return append(combined, proposed...) +} + func readSpecificRepositoryCarrier( root *os.Root, relative string, @@ -751,6 +771,7 @@ func inventoryRepositoryRuleBlocks( ByPath: make(map[string][]RepositoryRuleBlock), } existing := make(map[string]string) + existingBodies := make(map[string][][]byte) for _, relative := range sortedKeys(activeGuides) { content, err := readOptionalRegular(root, relative) if err != nil { @@ -770,6 +791,10 @@ func inventoryRepositoryRuleBlocks( ) } existing[span.ID] = relative + existingBodies[relative] = append( + existingBodies[relative], + append([]byte(nil), span.Body...), + ) inventory.ByPath[relative] = append( inventory.ByPath[relative], RepositoryRuleBlock{ @@ -807,6 +832,16 @@ func inventoryRepositoryRuleBlocks( } continue } + var bodyExists bool + for _, body := range existingBodies[block.Path] { + if bytes.Equal(body, block.Body) { + bodyExists = true + break + } + } + if bodyExists { + continue + } cloned := block cloned.Body = append([]byte(nil), block.Body...) inventory.ByPath[block.Path] = append(inventory.ByPath[block.Path], cloned) @@ -1888,9 +1923,13 @@ func assemblePostimages( return nil, nil, err } content := string(current) + if preservationConsumesRootPath(preservation, relative) { + content = "" + } if relative == "AGENTS.md" && !repositoryPlan.IncludeRoot { - withoutPointer := removeManagedBlock(content, repositoryExtensionRootID) - removedRepositoryPointer = withoutPointer != content + withoutPointer := removeManagedBlock(string(current), repositoryExtensionRootID) + removedRepositoryPointer = withoutPointer != string(current) + withoutPointer = removeManagedBlock(content, repositoryExtensionRootID) content = withoutPointer } for _, artifact := range grouped { @@ -2034,6 +2073,25 @@ func assemblePostimages( return postimages, ledger, nil } +func preservationConsumesRootPath(preservation RootPreservationPlan, relative string) bool { + if preservation.Mode != PreservationModePreservation || + preservation.State != PreservationStateReady { + return false + } + if _, consumed := preservation.consumedRootPaths[relative]; consumed { + return relative == "AGENTS.md" + } + if len(preservation.Dispositions) == 0 { + return false + } + for _, entry := range preservation.SourceBaseline.Entries { + if entry.Path == relative { + return relative == "AGENTS.md" + } + } + return false +} + func readOptionalRegular(root, relative string) ([]byte, error) { target := filepath.Join(root, filepath.FromSlash(relative)) info, err := os.Lstat(target) diff --git a/internal/baseline/plan_test.go b/internal/baseline/plan_test.go index ad549da1..4db5fd5f 100644 --- a/internal/baseline/plan_test.go +++ b/internal/baseline/plan_test.go @@ -753,6 +753,421 @@ func TestSemanticRuleDistributionMovesExactBytesAndAccountsLedgers(t *testing.T) } } +func TestPreservationSemanticRedistributionConverges(t *testing.T) { + repo := newPlanRepository(t) + rule := []byte("Keep the repository release name stable.\n") + writeInspectionFile(t, repo, "AGENTS.md", string(rule)) + commitInspectionRepository(t, repo, "seed root repository rule") + + plan := buildRootPreservationPlan( + t, + repo, + "repository-rules", + specificRepositoryPath, + ) + agents := planPostimage(t, plan, "AGENTS.md") + if bytes.Contains(agents.Content, rule) { + t.Fatalf("redistributed source remains in live AGENTS.md:\n%s", agents.Content) + } + if residual := planPostimage(t, plan, specificRepositoryPath); !bytes.Equal(residual.Content, rule) { + t.Fatalf("residual repository rules = %q, want %q", residual.Content, rule) + } + backup := planBackupEntry(t, plan, "AGENTS.md") + if _, err := ApplyPlan(context.Background(), repo, plan, plan.PlanDigest); err != nil { + t.Fatalf("apply root semantic redistribution: %v", err) + } + backupBytes, err := os.ReadFile(filepath.Join(repo, filepath.FromSlash(backup.Path))) + if err != nil { + t.Fatalf("read root backup: %v", err) + } + if !bytes.Equal(backupBytes, rule) { + t.Fatalf("root backup = %q, want exact source %q", backupBytes, rule) + } + + freshPreservation, err := PlanRootPreservation( + inspectPreservationRepository(t, repo), + RootPreservationRequest{Mode: PreservationModePreservation}, + ) + if err != nil { + t.Fatalf("plan fresh root preservation: %v", err) + } + if len(freshPreservation.SourceBaseline.Entries) != 0 { + t.Fatalf( + "fresh Preservation re-inventoried retained rules: entries=%+v", + freshPreservation.SourceBaseline.Entries, + ) + } + fresh := buildPreservationUpdatePlan(t, repo, nil) + if len(fresh.FileChanges) != 0 { + t.Fatalf("fresh Preservation file changes = %+v, want none", fresh.FileChanges) + } + for _, entry := range fresh.ManagedEntries { + if entry.Kind == "backup" { + t.Fatalf("fresh Preservation planned a second backup: %+v", entry) + } + } +} + +func TestPreservationLaterUnmarkedAdditionConverges(t *testing.T) { + repo := newPlanRepository(t) + initialRule := []byte("Keep CLI diagnostics on stderr for this repository.\n") + writeInspectionFile(t, repo, "AGENTS.md", string(initialRule)) + commitInspectionRepository(t, repo, "seed initial root repository rule") + initial := buildRootPreservationPlan(t, repo, "repository-rules", specificRepositoryPath) + if _, err := ApplyPlan(context.Background(), repo, initial, initial.PlanDigest); err != nil { + t.Fatalf("apply initial semantic redistribution: %v", err) + } + + laterRule := []byte("Keep the public CLI help concise.\n") + agentsPath := filepath.Join(repo, "AGENTS.md") + agents, err := os.ReadFile(agentsPath) + if err != nil { + t.Fatalf("read managed AGENTS.md: %v", err) + } + preMigration := append(append([]byte(nil), agents...), laterRule...) + writeTransactionFile(t, repo, "AGENTS.md", string(preMigration), 0o644) + commitInspectionRepository(t, repo, "add later unmarked root rule") + + unresolved, err := PlanRootPreservation( + inspectPreservationRepository(t, repo), + RootPreservationRequest{Mode: PreservationModePreservation}, + ) + if err != nil { + t.Fatalf("plan later root preservation: %v", err) + } + if len(unresolved.SourceBaseline.Entries) != 1 || + !bytes.Equal(unresolved.SourceBaseline.Entries[0].SourceBytes, laterRule) { + t.Fatalf( + "later Source Baseline entries = %+v, want only %q", + unresolved.SourceBaseline.Entries, + laterRule, + ) + } + later := buildRootPreservationPlan(t, repo, "repository-document", "docs/agents/cli.md") + backup := planBackupEntry(t, later, "AGENTS.md") + if backup.ContentIdentity != planContentIdentity(preMigration) { + t.Fatalf( + "later backup identity = %q, want complete root identity %q", + backup.ContentIdentity, + planContentIdentity(preMigration), + ) + } + if _, err := ApplyPlan(context.Background(), repo, later, later.PlanDigest); err != nil { + t.Fatalf("apply later semantic redistribution: %v", err) + } + appliedAgents, err := os.ReadFile(agentsPath) + if err != nil { + t.Fatalf("read converged AGENTS.md: %v", err) + } + if bytes.Contains(appliedAgents, laterRule) { + t.Fatalf("later source remains in live AGENTS.md:\n%s", appliedAgents) + } + guide, err := os.ReadFile(filepath.Join(repo, "docs", "agents", "cli.md")) + if err != nil { + t.Fatalf("read CLI guide: %v", err) + } + if !bytes.Contains(guide, laterRule) { + t.Fatalf("CLI guide does not retain later rule:\n%s", guide) + } + residual, err := os.ReadFile(filepath.Join(repo, filepath.FromSlash(specificRepositoryPath))) + if err != nil { + t.Fatalf("read retained Repository-Specific Normative Rules: %v", err) + } + if !bytes.Equal(residual, initialRule) { + t.Fatalf("existing Repository-Specific Normative Rules = %q, want %q", residual, initialRule) + } + + fresh := buildPreservationUpdatePlan(t, repo, nil) + if len(fresh.FileChanges) != 0 { + t.Fatalf("post-addition Preservation file changes = %+v, want none", fresh.FileChanges) + } + for _, entry := range fresh.ManagedEntries { + if entry.Kind == "backup" { + t.Fatalf("post-addition Preservation planned another backup: %+v", entry) + } + } + + residualRule := []byte("Keep the project-specific release channel stable.\n") + appliedAgents, err = os.ReadFile(agentsPath) + if err != nil { + t.Fatalf("read managed AGENTS.md before residual addition: %v", err) + } + writeTransactionFile( + t, + repo, + "AGENTS.md", + string(append(appliedAgents, residualRule...)), + 0o644, + ) + commitInspectionRepository(t, repo, "add later residual root rule") + residualPlan := buildRootPreservationPlan( + t, + repo, + "repository-rules", + specificRepositoryPath, + ) + if _, err := ApplyPlan( + context.Background(), + repo, + residualPlan, + residualPlan.PlanDigest, + ); err != nil { + t.Fatalf("apply later residual redistribution: %v", err) + } + residual, err = os.ReadFile(filepath.Join(repo, filepath.FromSlash(specificRepositoryPath))) + if err != nil { + t.Fatalf("read extended Repository-Specific Normative Rules: %v", err) + } + wantResidual := append(append([]byte(nil), initialRule...), residualRule...) + if !bytes.Equal(residual, wantResidual) { + t.Fatalf("extended Repository-Specific Normative Rules = %q, want %q", residual, wantResidual) + } + final := buildPreservationUpdatePlan(t, repo, nil) + if len(final.FileChanges) != 0 { + t.Fatalf("final Preservation file changes = %+v, want none", final.FileChanges) + } +} + +func TestPreservationPreviouslyBackedUpRootGuidanceIsNotReclassified(t *testing.T) { + tests := []struct { + name string + disposition string + path string + }{ + { + name: "semantic guide", + disposition: "repository-document", + path: "docs/agents/cli.md", + }, + { + name: "residual carrier", + disposition: "repository-rules", + path: specificRepositoryPath, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + repo := newPlanRepository(t) + rule := []byte("Keep the duplicate migration rule single-owned.\n") + writeInspectionFile(t, repo, "AGENTS.md", string(rule)) + commitInspectionRepository(t, repo, "seed root rule") + initial := buildRootPreservationPlan( + t, + repo, + test.disposition, + test.path, + ) + if _, err := ApplyPlan( + context.Background(), + repo, + initial, + initial.PlanDigest, + ); err != nil { + t.Fatalf("apply initial owner migration: %v", err) + } + + agents, err := os.ReadFile(filepath.Join(repo, "AGENTS.md")) + if err != nil { + t.Fatalf("read managed AGENTS.md: %v", err) + } + ownerBefore, err := os.ReadFile(filepath.Join(repo, filepath.FromSlash(test.path))) + if err != nil { + t.Fatalf("read approved owner: %v", err) + } + staleRoot := append(append([]byte(nil), agents...), rule...) + writeTransactionFile( + t, + repo, + "AGENTS.md", + string(staleRoot), + 0o644, + ) + commitInspectionRepository(t, repo, "restore already-owned root bytes") + retains, err := currentSetupRetainsRecognizedRepositoryRules(repo) + if err != nil { + t.Fatalf("resolve current Setup Manifest ownership: %v", err) + } + if !retains { + t.Fatal("initial applied Setup Manifest did not prove current ownership") + } + payloads, err := verifiedAgentsBackupPayloads(repo) + if err != nil { + t.Fatalf("load verified AGENTS backups: %v", err) + } + if len(payloads) == 0 || !bytes.Equal(payloads[0], rule) { + t.Fatalf("verified AGENTS backup payloads = %q, want %q", payloads, rule) + } + unresolved, err := PlanRootPreservation( + inspectPreservationRepository(t, repo), + RootPreservationRequest{Mode: PreservationModePreservation}, + ) + if err != nil { + t.Fatalf("plan stale-root repair: %v", err) + } + if len(unresolved.SourceBaseline.Entries) != 0 { + t.Fatalf( + "previously backed-up root bytes re-entered classification: %+v", + unresolved.SourceBaseline.Entries, + ) + } + if _, consumed := unresolved.consumedRootPaths["AGENTS.md"]; !consumed { + t.Fatal("previously backed-up root bytes did not mark AGENTS.md for repair") + } + repair := buildPreservationUpdatePlan(t, repo, nil) + if bytes.Contains(planPostimage(t, repair, "AGENTS.md").Content, rule) { + t.Fatal("already-owned source remains in the live root postimage") + } + if owner := planPostimage(t, repair, test.path); !bytes.Equal(owner.Content, ownerBefore) { + t.Fatalf("approved owner changed during stale-root repair:\n%s", owner.Content) + } + backup := planBackupEntry(t, repair, "AGENTS.md") + if backup.ContentIdentity != planContentIdentity(staleRoot) { + t.Fatalf( + "stale-root backup identity = %q, want %q", + backup.ContentIdentity, + planContentIdentity(staleRoot), + ) + } + if _, err := ApplyPlan( + context.Background(), + repo, + repair, + repair.PlanDigest, + ); err != nil { + t.Fatalf("apply stale-root repair: %v", err) + } + fresh := buildPreservationUpdatePlan(t, repo, nil) + if len(fresh.FileChanges) != 0 { + t.Fatalf("repaired fresh Plan changes = %+v, want none", fresh.FileChanges) + } + }) + } +} + +func TestPreservationPreviouslyBackedUpRootGuidanceExposesOnlyLaterAddition(t *testing.T) { + repo := newPlanRepository(t) + approved := []byte("Keep the already approved root rule.\n") + writeInspectionFile(t, repo, "AGENTS.md", string(approved)) + commitInspectionRepository(t, repo, "seed approved root rule") + initial := buildRootPreservationPlan( + t, + repo, + "repository-rules", + specificRepositoryPath, + ) + if _, err := ApplyPlan(context.Background(), repo, initial, initial.PlanDigest); err != nil { + t.Fatalf("apply initial owner migration: %v", err) + } + + agents, err := os.ReadFile(filepath.Join(repo, "AGENTS.md")) + if err != nil { + t.Fatalf("read managed AGENTS.md: %v", err) + } + later := []byte("Keep the later root rule separately reviewed.\n") + staleAndLater := append(append(append([]byte(nil), agents...), approved...), later...) + writeTransactionFile(t, repo, "AGENTS.md", string(staleAndLater), 0o644) + commitInspectionRepository(t, repo, "restore old bytes and add one rule") + + unresolved, err := PlanRootPreservation( + inspectPreservationRepository(t, repo), + RootPreservationRequest{Mode: PreservationModePreservation}, + ) + if err != nil { + t.Fatalf("plan mixed stale and new root guidance: %v", err) + } + if len(unresolved.SourceBaseline.Entries) != 1 || + !bytes.Equal(unresolved.SourceBaseline.Entries[0].SourceBytes, later) { + t.Fatalf( + "mixed Source Baseline entries = %+v, want only %q", + unresolved.SourceBaseline.Entries, + later, + ) + } + if _, consumed := unresolved.consumedRootPaths["AGENTS.md"]; !consumed { + t.Fatal("mixed stale and new root guidance did not retain stale-root repair") + } +} + +func buildRootPreservationPlan( + t *testing.T, + repo string, + disposition string, + destinationPath string, +) PlanDocument { + t.Helper() + unresolved, err := PlanRootPreservation( + inspectPreservationRepository(t, repo), + RootPreservationRequest{Mode: PreservationModePreservation}, + ) + if err != nil { + t.Fatalf("plan unresolved root preservation: %v", err) + } + if len(unresolved.SourceBaseline.Entries) != 1 { + t.Fatalf("root Source Baseline entries = %+v, want one", unresolved.SourceBaseline.Entries) + } + entry := unresolved.SourceBaseline.Entries[0] + destination := &ReadoptionDestination{ + Digest: entry.Digest, + Path: destinationPath, + } + switch disposition { + case "repository-document": + destination.DocumentType = "agent-guide" + case "repository-rules": + destination.DocumentType = "repository-rules" + destination.ProposedBytes = base64.StdEncoding.EncodeToString(entry.SourceBytes) + default: + t.Fatalf("unsupported test disposition %q", disposition) + } + document := decisionDocumentForSource( + unresolved.SourceBaseline, + []ReadoptionDisposition{{ + EntryID: entry.ID, + EntryDigest: entry.Digest, + Classification: "normative-clause", + Disposition: disposition, + Destination: destination, + Reason: "The selected repository owner retains this exact rule.", + }}, + ) + return buildPreservationUpdatePlan(t, repo, &document) +} + +func buildPreservationUpdatePlan( + t *testing.T, + repo string, + document *DecisionDocument, +) PlanDocument { + t.Helper() + outcome, err := BuildPlan(context.Background(), PlanRequest{ + Repository: repo, + ProfileID: "go-cli-tui", + Decisions: planTestDecisionsWithRepositoryExtension(), + Preservation: RootPreservationRequest{ + Mode: PreservationModePreservation, + Decisions: document, + }, + }) + if err != nil { + t.Fatalf("build Preservation update Plan: %v", err) + } + if outcome.Plan == nil { + t.Fatalf("build Preservation update returned result: %+v", outcome.Result) + } + return *outcome.Plan +} + +func planBackupEntry(t *testing.T, plan PlanDocument, sourcePath string) ManagedEntry { + t.Helper() + for _, entry := range plan.ManagedEntries { + if entry.ID == "backup:"+sourcePath && entry.Kind == "backup" { + return entry + } + } + t.Fatalf("Plan has no backup for %q", sourcePath) + return ManagedEntry{} +} + func decisionDocumentForSource( source ReadoptionSourceBaseline, dispositions []ReadoptionDisposition, diff --git a/internal/baseline/preservation.go b/internal/baseline/preservation.go index 35a442df..4541b81a 100644 --- a/internal/baseline/preservation.go +++ b/internal/baseline/preservation.go @@ -193,6 +193,8 @@ type RootPreservationPlan struct { Findings []Finding `json:"findings"` DecisionSkeleton *DecisionSkeleton `json:"decisionSkeleton,omitempty"` NextAction string `json:"nextAction,omitempty"` + + consumedRootPaths map[string]struct{} } // ParseDecisionDocument parses the maintained strict Decision Document @@ -542,17 +544,34 @@ func PlanRootPreservation( return RootPreservationPlan{}, err } plan.Findings = append(plan.Findings, findings...) - plan.Backups, findings, err = planRootBackups(inspection.Root, rootSources) + retainsRepositoryRules, err := currentSetupRetainsRecognizedRepositoryRules(inspection.Root) if err != nil { return RootPreservationPlan{}, err } - plan.Findings = append(plan.Findings, findings...) - repositorySources, findings, err := loadRecognizedRepositoryRuleSources(inspection.Root) + if request.Mode == PreservationModePreservation && retainsRepositoryRules { + rootSources, plan.consumedRootPaths, err = excludePreviouslyBackedUpRootGuidance( + inspection.Root, + rootSources, + ) + if err != nil { + return RootPreservationPlan{}, err + } + } + migrationRootSources := rootSourcesWithUnmarkedGuidance(rootSources) + plan.Backups, findings, err = planRootBackups(inspection.Root, rootSources) if err != nil { return RootPreservationPlan{}, err } plan.Findings = append(plan.Findings, findings...) - sources := append(rootSources, repositorySources...) + var repositorySources []rootPreservationSource + if !retainsRepositoryRules { + repositorySources, findings, err = loadRecognizedRepositoryRuleSources(inspection.Root) + if err != nil { + return RootPreservationPlan{}, err + } + plan.Findings = append(plan.Findings, findings...) + } + sources := append(migrationRootSources, repositorySources...) plan.SourceBaseline = buildReadoptionSourceBaseline(sources) plan.Findings = sortedFindings(plan.Findings) @@ -611,6 +630,8 @@ type rootPreservationSource struct { sourcePath string contentIdentity string content []byte + + classificationEntries []ReadoptionSourceEntry } func loadRootPreservationSources( @@ -709,6 +730,186 @@ func loadRecognizedRepositoryRuleSources( return sources, findings, nil } +func currentSetupRetainsRecognizedRepositoryRules(rootPath string) (bool, error) { + manifestBytes, err := readOptionalRegular(rootPath, manifestPath) + if err != nil { + return false, fmt.Errorf("read current Setup Manifest ownership: %w", err) + } + if len(manifestBytes) == 0 { + return false, nil + } + var manifest SetupManifest + if err := json.Unmarshal(manifestBytes, &manifest); err != nil { + return false, nil + } + if manifest.SchemaVersion != ManifestSchema || + manifest.Version != ManifestVersion || + manifest.Generator.Skill != "setup-context-driven" || + manifest.Generator.Version != ManifestVersion || + manifest.Generator.Baseline != "baseline."+manifest.Profile+"-"+ManifestVersion { + return false, nil + } + catalog, err := LoadEmbeddedCatalog() + if err != nil { + return false, fmt.Errorf("load catalog for Setup Manifest ownership: %w", err) + } + if manifest.CatalogDigest != catalog.Digest() { + return false, nil + } + profile, err := ResolveProfile(rootPath, manifest.Profile, catalog) + if err != nil || profile.Digest != manifest.ProfileDigest { + return false, nil + } + return true, nil +} + +func rootSourcesWithUnmarkedGuidance( + sources []rootPreservationSource, +) []rootPreservationSource { + selected := make([]rootPreservationSource, 0, len(sources)) + for _, source := range sources { + if len(classificationEntries(source)) == 0 { + continue + } + selected = append(selected, source) + } + return selected +} + +var agentsBackupName = regexp.MustCompile(`^AGENTS\.([a-f0-9]{64})\.md$`) + +func excludePreviouslyBackedUpRootGuidance( + rootPath string, + sources []rootPreservationSource, +) ([]rootPreservationSource, map[string]struct{}, error) { + payloads, err := verifiedAgentsBackupPayloads(rootPath) + if err != nil { + return nil, nil, err + } + consumed := make(map[string]struct{}) + for index := range sources { + source := &sources[index] + if source.carrierPath != "AGENTS.md" || + source.sourcePath != "AGENTS.md" || + !containsSetupManagedGuidance(source.content) { + continue + } + entries, excluded := excludeBackedUpPayloads( + unmarkedSourceEntries(source.sourcePath, source.content), + payloads, + ) + source.classificationEntries = entries + if excluded { + consumed[source.sourcePath] = struct{}{} + } + } + return sources, consumed, nil +} + +func verifiedAgentsBackupPayloads(rootPath string) ([][]byte, error) { + entries, err := os.ReadDir(rootPath) + if err != nil { + return nil, fmt.Errorf("list prior AGENTS backups: %w", err) + } + anchored, err := os.OpenRoot(rootPath) + if err != nil { + return nil, fmt.Errorf("open repository root for prior AGENTS backups: %w", err) + } + defer anchored.Close() + + var payloads [][]byte + for _, entry := range entries { + match := agentsBackupName.FindStringSubmatch(entry.Name()) + if len(match) != 2 || entry.Type()&fs.ModeSymlink != 0 { + continue + } + payload, readErr := readRootRegularFile(anchored, entry.Name()) + if readErr != nil { + continue + } + sum := sha256.Sum256(payload) + if hex.EncodeToString(sum[:]) != match[1] { + continue + } + payloads = append(payloads, payload) + } + sort.Slice(payloads, func(i, j int) bool { + if len(payloads[i]) != len(payloads[j]) { + return len(payloads[i]) > len(payloads[j]) + } + return bytes.Compare(payloads[i], payloads[j]) < 0 + }) + return payloads, nil +} + +func excludeBackedUpPayloads( + entries []ReadoptionSourceEntry, + payloads [][]byte, +) ([]ReadoptionSourceEntry, bool) { + result := make([]ReadoptionSourceEntry, 0, len(entries)) + var excluded bool + for _, entry := range entries { + ranges := [][2]int{{0, len(entry.SourceBytes)}} + for _, payload := range payloads { + if len(payload) == 0 { + continue + } + var remaining [][2]int + for _, current := range ranges { + cursor := current[0] + for cursor < current[1] { + offset := bytes.Index(entry.SourceBytes[cursor:current[1]], payload) + if offset < 0 { + break + } + start := cursor + offset + if start > cursor { + remaining = append(remaining, [2]int{cursor, start}) + } + cursor = start + len(payload) + excluded = true + } + if cursor < current[1] { + remaining = append(remaining, [2]int{cursor, current[1]}) + } + } + ranges = remaining + } + for _, current := range ranges { + sourceBytes := entry.SourceBytes[current[0]:current[1]] + if len(bytes.TrimSpace(sourceBytes)) == 0 { + continue + } + result = append(result, newReadoptionSourceEntry( + entry.Path, + entry.Kind, + entry.Start+current[0], + entry.Start+current[1], + sourceBytes, + entry.CarrierDigest, + entry.StructuralProvenance, + )) + } + } + return result, excluded +} + +func containsSetupManagedGuidance(content []byte) bool { + for _, entry := range partitionRootSource("", content) { + if entry.Kind == "managed-block" { + return true + } + } + return false +} + +func classificationEntries(source rootPreservationSource) []ReadoptionSourceEntry { + if source.classificationEntries != nil { + return source.classificationEntries + } + return unmarkedSourceEntries(source.sourcePath, source.content) +} + func validateClassifiedSourceBaseline( current ReadoptionSourceBaseline, classified ReadoptionSourceBaseline, @@ -871,14 +1072,22 @@ func buildReadoptionSourceBaseline(sources []rootPreservationSource) ReadoptionS identity := sha256.New() var entries []ReadoptionSourceEntry byteCount := 0 + carrierCount := 0 for _, source := range sources { + sourceEntries := classificationEntries(source) + if len(sourceEntries) == 0 { + continue + } writeLengthPrefixed(identity, source.sourcePath) var length [8]byte binary.BigEndian.PutUint64(length[:], uint64(len(source.content))) _, _ = identity.Write(length[:]) _, _ = identity.Write(source.content) - byteCount += len(source.content) - entries = append(entries, partitionRootSource(source.sourcePath, source.content)...) + carrierCount++ + for _, entry := range sourceEntries { + byteCount += len(entry.SourceBytes) + entries = append(entries, entry) + } } sort.Slice(entries, func(i, j int) bool { if entries[i].Path != entries[j].Path { @@ -898,7 +1107,7 @@ func buildReadoptionSourceBaseline(sources []rootPreservationSource) ReadoptionS DeclaredIdentity: "unconfigured", Compatibility: "incompatible", Digest: digest, - CarrierCount: len(sources), + CarrierCount: carrierCount, EntryCount: len(entries), ByteCount: byteCount, Entries: entries, @@ -1011,6 +1220,18 @@ func partitionRootSource(sourcePath string, content []byte) []ReadoptionSourceEn return entries } +func unmarkedSourceEntries(sourcePath string, content []byte) []ReadoptionSourceEntry { + partitioned := partitionRootSource(sourcePath, content) + entries := make([]ReadoptionSourceEntry, 0, len(partitioned)) + for _, entry := range partitioned { + if entry.Kind == "managed-block" || len(bytes.TrimSpace(entry.SourceBytes)) == 0 { + continue + } + entries = append(entries, entry) + } + return entries +} + func containsOnlySetupManagedGuidance(content []byte) bool { entries := partitionRootSource("", content) hasManaged := false diff --git a/internal/baseline/preservation_test.go b/internal/baseline/preservation_test.go index 57d82f7a..ddb66c59 100644 --- a/internal/baseline/preservation_test.go +++ b/internal/baseline/preservation_test.go @@ -212,13 +212,10 @@ func TestDecisionDocumentSkeletonDoesNotProposeManagedSemanticVersionBytes(t *te if err != nil { t.Fatalf("plan managed-root preservation: %v", err) } - if plan.DecisionSkeleton == nil { - t.Fatalf("managed-root preservation has no Decision Document skeleton: %+v", plan) - } - for _, disposition := range plan.DecisionSkeleton.Document.Readoption.Dispositions { - if disposition.Disposition == "repository-rules" || disposition.Destination != nil { - t.Fatalf("setup-managed bytes were proposed as repository rules: %+v", disposition) - } + if plan.State != PreservationStateReady || + plan.DecisionSkeleton != nil || + len(plan.SourceBaseline.Entries) != 0 { + t.Fatalf("setup-managed bytes entered classification: %+v", plan) } } diff --git a/internal/baseline/segmentation.go b/internal/baseline/segmentation.go index 73249d42..f3bdb264 100644 --- a/internal/baseline/segmentation.go +++ b/internal/baseline/segmentation.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "unicode/utf8" ) const ( @@ -34,16 +35,33 @@ type RuleSegmentationSnapshot struct { ProposalSchema string `json:"proposalSchema"` ProposalContract RuleSegmentationProposalContract `json:"proposalContract"` SourceBaseline ReadoptionSourceBaseline `json:"sourceBaseline"` + SemanticEntries []RuleSegmentationSemanticEntry `json:"semanticEntries"` SnapshotDigest string `json:"snapshotDigest"` } +// RuleSegmentationSemanticEntry gives a sealed analyzer readable source text +// and exact line-boundary offsets without making text part of the proposal. +type RuleSegmentationSemanticEntry struct { + EntryID string `json:"entryId"` + Readable bool `json:"readable"` + Lines []RuleSegmentationSemanticLine `json:"lines"` +} + +// RuleSegmentationSemanticLine binds readable text to exact source byte +// offsets so the analyzer never has to count or reconstruct boundaries. +type RuleSegmentationSemanticLine struct { + Start int `json:"start"` + End int `json:"end"` + Text string `json:"text"` +} + // RuleSegmentProposal identifies one byte range inside one structural Source // Baseline Entry. It never carries proposed or rewritten content. type RuleSegmentProposal struct { EntryID string `json:"entryId"` Start int `json:"start"` End int `json:"end"` - Digest string `json:"digest"` + Digest string `json:"digest,omitempty"` } // RuleSegmentationProposal is a complete byte-exhaustive proposal bound to one @@ -51,7 +69,7 @@ type RuleSegmentProposal struct { type RuleSegmentationProposal struct { SchemaVersion string `json:"schemaVersion"` SnapshotDigest string `json:"snapshotDigest"` - SourceBaseline ClassificationSource `json:"sourceBaseline"` + SourceBaseline ClassificationSource `json:"sourceBaseline,omitempty"` Segments []RuleSegmentProposal `json:"segments"` } @@ -72,6 +90,7 @@ func NewRuleSegmentationSnapshot( ProposalSchema: RuleSegmentationProposalSchemaVersion, ProposalContract: ruleSegmentationProposalContract(), SourceBaseline: cloneReadoptionSourceBaseline(source), + SemanticEntries: ruleSegmentationSemanticEntries(source.Entries), } digest, err := computeRuleSegmentationSnapshotDigest(snapshot) if err != nil { @@ -165,8 +184,13 @@ func normalizeRuleSegmentationProposal( "validate Segmentation Proposal: Segmentation Snapshot digest does not match", ) } - if proposal.SourceBaseline.ID != snapshot.SourceBaseline.ID || - proposal.SourceBaseline.Digest != snapshot.SourceBaseline.Digest { + expectedSourceBaseline := ClassificationSource{ + ID: snapshot.SourceBaseline.ID, + Digest: snapshot.SourceBaseline.Digest, + } + if proposal.SourceBaseline == (ClassificationSource{}) { + proposal.SourceBaseline = expectedSourceBaseline + } else if proposal.SourceBaseline != expectedSourceBaseline { return RuleSegmentationProposal{}, errors.New( "validate Segmentation Proposal: Source Baseline identity does not match", ) @@ -198,6 +222,15 @@ func normalizeRuleSegmentationProposal( } entries := make(map[string]ReadoptionSourceEntry, len(snapshot.SourceBaseline.Entries)) + boundaries := make(map[string]map[int]struct{}, len(snapshot.SemanticEntries)) + for _, semanticEntry := range snapshot.SemanticEntries { + entryBoundaries := make(map[int]struct{}, len(semanticEntry.Lines)+1) + for _, line := range semanticEntry.Lines { + entryBoundaries[line.Start] = struct{}{} + entryBoundaries[line.End] = struct{}{} + } + boundaries[semanticEntry.EntryID] = entryBoundaries + } for _, entry := range snapshot.SourceBaseline.Entries { entries[entry.ID] = entry } @@ -210,6 +243,8 @@ func normalizeRuleSegmentationProposal( } } + normalized := proposal + normalized.Segments = append([]RuleSegmentProposal(nil), proposal.Segments...) segmentIndex := 0 for _, entry := range snapshot.SourceBaseline.Entries { if len(entry.SourceBytes) == 0 { @@ -232,7 +267,7 @@ func normalizeRuleSegmentationProposal( cursor := 0 for segmentIndex < len(proposal.Segments) && proposal.Segments[segmentIndex].EntryID == entry.ID { - segment := proposal.Segments[segmentIndex] + segment := normalized.Segments[segmentIndex] if segment.Start != cursor { return RuleSegmentationProposal{}, fmt.Errorf( "validate Segmentation Proposal: range [%d,%d) for %q starts at %d; expected %d", @@ -260,12 +295,28 @@ func normalizeRuleSegmentationProposal( len(entry.SourceBytes), ) } - if segment.Digest != ruleSegmentDigest(entry.SourceBytes[segment.Start:segment.End]) { + if _, ok := boundaries[entry.ID][segment.Start]; !ok { + return RuleSegmentationProposal{}, fmt.Errorf( + "validate Segmentation Proposal: range start %d for %q is not an advertised boundary", + segment.Start, + entry.ID, + ) + } + if _, ok := boundaries[entry.ID][segment.End]; !ok { + return RuleSegmentationProposal{}, fmt.Errorf( + "validate Segmentation Proposal: range end %d for %q is not an advertised boundary", + segment.End, + entry.ID, + ) + } + expectedDigest := ruleSegmentDigest(entry.SourceBytes[segment.Start:segment.End]) + if segment.Digest != "" && segment.Digest != expectedDigest { return RuleSegmentationProposal{}, fmt.Errorf( "validate Segmentation Proposal: range digest for %q does not match sealed source bytes", entry.ID, ) } + normalized.Segments[segmentIndex].Digest = expectedDigest cursor = segment.End segmentIndex++ } @@ -284,8 +335,6 @@ func normalizeRuleSegmentationProposal( ) } - normalized := proposal - normalized.Segments = append([]RuleSegmentProposal(nil), proposal.Segments...) return normalized, nil } @@ -392,6 +441,12 @@ func validateRuleSegmentationSnapshot(snapshot RuleSegmentationSnapshot) error { err, ) } + if !equalRuleSegmentationSemanticEntries( + snapshot.SemanticEntries, + ruleSegmentationSemanticEntries(snapshot.SourceBaseline.Entries), + ) { + return errors.New("validate Segmentation Snapshot: semantic entries changed") + } digest, err := computeRuleSegmentationSnapshotDigest(snapshot) if err != nil { return err @@ -425,24 +480,115 @@ func ruleSegmentationProposalContract() RuleSegmentationProposalContract { RequiredFields: []string{ "schemaVersion", "snapshotDigest", - "sourceBaseline", "segments", }, RequiredSegmentFields: []string{ "entryId", "start", "end", - "digest", }, Rules: []string{ "Return exactly one JSON object with no prose.", "Return only ordered non-empty byte ranges for advertised entryId values.", + "Use only start and end offsets advertised by semanticEntries lines.", + "Use each semanticEntries line text together with its exact start and end offsets; the sealed snapshot remains the authoritative byte evidence.", + "Treat readable=false as unavailable semantic text and return one complete range for that entry.", + "Split each readable entry into the smallest coherent instruction clauses that can be classified and routed independently.", + "Keep Markdown headings with the content they introduce and group adjacent lines only when separating them would change their meaning.", + "Do not return one full-entry range when readable text contains multiple independently enforceable instructions.", + "Attach whitespace separators to an adjacent clause so every byte remains covered.", "Cover every byte of every non-empty entry exactly once without gaps, overlap, duplication, or reordering.", - "Copy the lowercase SHA-256 digest of each proposed range; never return source content.", + "Do not return sourceBaseline, source content, or any digest except snapshotDigest; the planner derives source identity, source content, and segment digests locally.", }, } } +func ruleSegmentationSemanticEntries( + entries []ReadoptionSourceEntry, +) []RuleSegmentationSemanticEntry { + result := make([]RuleSegmentationSemanticEntry, len(entries)) + for index, entry := range entries { + readable := utf8.Valid(entry.SourceBytes) + result[index] = RuleSegmentationSemanticEntry{ + EntryID: entry.ID, + Readable: readable, + Lines: ruleSegmentationSemanticLines(entry.SourceBytes, readable), + } + } + return result +} + +func ruleSegmentationSemanticLines( + source []byte, + readable bool, +) []RuleSegmentationSemanticLine { + if len(source) == 0 { + return []RuleSegmentationSemanticLine{} + } + if !readable { + return []RuleSegmentationSemanticLine{{ + Start: 0, + End: len(source), + }} + } + lines := make([]RuleSegmentationSemanticLine, 0) + start := 0 + for index, current := range source { + if current == '\n' { + end := index + 1 + lines = append(lines, RuleSegmentationSemanticLine{ + Start: start, + End: end, + Text: string(source[start:end]), + }) + start = end + } + } + if start != len(source) { + lines = append(lines, RuleSegmentationSemanticLine{ + Start: start, + End: len(source), + Text: string(source[start:]), + }) + } + return lines +} + +func equalRuleSegmentationSemanticEntries( + first []RuleSegmentationSemanticEntry, + second []RuleSegmentationSemanticEntry, +) bool { + if first == nil || len(first) != len(second) { + return false + } + for index := range first { + if first[index].EntryID != second[index].EntryID || + first[index].Readable != second[index].Readable || + !equalRuleSegmentationSemanticLines( + first[index].Lines, + second[index].Lines, + ) { + return false + } + } + return true +} + +func equalRuleSegmentationSemanticLines( + first []RuleSegmentationSemanticLine, + second []RuleSegmentationSemanticLine, +) bool { + if first == nil || len(first) != len(second) { + return false + } + for index := range first { + if first[index] != second[index] { + return false + } + } + return true +} + func equalRuleSegmentationProposalContract( first RuleSegmentationProposalContract, second RuleSegmentationProposalContract, diff --git a/internal/baseline/segmentation_test.go b/internal/baseline/segmentation_test.go index fc98354c..f9af2db4 100644 --- a/internal/baseline/segmentation_test.go +++ b/internal/baseline/segmentation_test.go @@ -59,6 +59,51 @@ func TestSegmentationSnapshotIsCanonicalAndCheckoutFree(t *testing.T) { } } +func TestSegmentationSnapshotMakesExactTextAndBoundariesAvailableToSealedAnalysis(t *testing.T) { + snapshot := segmentationTestSnapshot(t) + canonical, err := snapshot.CanonicalBytes() + if err != nil { + t.Fatalf("marshal Segmentation Snapshot: %v", err) + } + entry := snapshot.SourceBaseline.Entries[0] + for _, required := range []string{ + `"semanticEntries"`, + `"entryId":"` + entry.ID + `"`, + `"lines":[{"start":0,"end":11,"text":"first rule\n"},{"start":11,"end":12,"text":"\n"},{"start":12,"end":24,"text":"second rule\n"}]`, + `Split each readable entry into the smallest coherent instruction clauses`, + } { + if !bytes.Contains(canonical, []byte(required)) { + t.Fatalf("Segmentation Snapshot does not expose %q:\n%s", required, canonical) + } + } + + proposal := map[string]any{ + "schemaVersion": RuleSegmentationProposalSchemaVersion, + "snapshotDigest": snapshot.SnapshotDigest, + "segments": []map[string]any{ + {"entryId": entry.ID, "start": 0, "end": 11}, + {"entryId": entry.ID, "start": 11, "end": len(entry.SourceBytes)}, + }, + } + payload, err := json.Marshal(proposal) + if err != nil { + t.Fatal(err) + } + parsed, err := ParseRuleSegmentationProposal(payload, snapshot) + if err != nil { + t.Fatalf("range-only Segmentation Proposal was rejected: %v", err) + } + if parsed.SourceBaseline.ID != snapshot.SourceBaseline.ID || + parsed.SourceBaseline.Digest != snapshot.SourceBaseline.Digest { + t.Fatalf("Source Baseline identity was not derived locally: %+v", parsed.SourceBaseline) + } + for index, segment := range parsed.Segments { + if segment.Digest == "" { + t.Fatalf("segment %d digest was not derived locally: %+v", index, segment) + } + } +} + func TestSegmentationProposalRejectsInvalidRangesAndStaleIdentity(t *testing.T) { snapshot := segmentationTestSnapshot(t) valid := segmentationTestProposal(t, snapshot, 11) @@ -88,6 +133,16 @@ func TestSegmentationProposalRejectsInvalidRangesAndStaleIdentity(t *testing.T) return proposal }, }, + { + name: "unadvertised boundary", + mutate: func(proposal RuleSegmentationProposal) RuleSegmentationProposal { + proposal.Segments[0].End = 10 + proposal.Segments[0].Digest = "" + proposal.Segments[1].Start = 10 + proposal.Segments[1].Digest = "" + return proposal + }, + }, { name: "duplicate", mutate: func(proposal RuleSegmentationProposal) RuleSegmentationProposal { diff --git a/internal/baseline/testdata/catalog.digest b/internal/baseline/testdata/catalog.digest index 034f659f..2a36b15a 100644 --- a/internal/baseline/testdata/catalog.digest +++ b/internal/baseline/testdata/catalog.digest @@ -1 +1 @@ -sha256:b6ac508a632f8bff55f568443f75304389ae824fae6097475dd926255245b006 +sha256:f082de03ea52e1b17e75daf0d65ef2d535f8d8d36951fe08dd435732e4008c1e diff --git a/internal/baseline/testdata/catalog.normalized.json b/internal/baseline/testdata/catalog.normalized.json index 5bb98589..369c4108 100644 --- a/internal/baseline/testdata/catalog.normalized.json +++ b/internal/baseline/testdata/catalog.normalized.json @@ -1 +1 @@ -{"schemaVersion":"roundfix/baseline-catalog/v1","files":[{"path":"contract-v1.json","bytes":13525,"digest":"sha256:0c32b7ac1b6dd1a2a04d830b300d592b7b195aec7bb32ff05575e8d19f154497"},{"path":"coverage.json","bytes":4660,"digest":"sha256:9faa73bf86fad383570b0098a9aa26185ea8d689f9866038ab37b8a34b9a01dc"},{"path":"decisions.json","bytes":8438,"digest":"sha256:58acbb05cf46057923e8844b28e2a97efa898d6200ae9e0773971b6287b05651"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/AGENTS.md","bytes":2911,"digest":"sha256:1876cc64ced89e60cfb0188eaff71d8b588c80d1149740ba368d1afc969e1d8f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/agent-instructions.md","bytes":4182,"digest":"sha256:eb769f99afa32a901723e1962a06fca09d856c8d37284b36c9aed071bd6e4f86"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/autonomous-work.md","bytes":789,"digest":"sha256:76065b3f1b452f647765d01bfb802ca3a0757d7212ba75b8e55d1d6242b0c977"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/backend.md","bytes":1083,"digest":"sha256:b8459f66c5fec1d424768fdd2b0b2f4ec70b9038208c1d5a58086720586195fd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/docs-layout.md","bytes":3546,"digest":"sha256:a188cfaae89ed195ca31460af3ec5d0dfad0bc1241e1958950ed4e0035de86f6"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/domain.md","bytes":614,"digest":"sha256:089a408493f450ef5713f3f62b20b51323b6d08617e6e43a12e38d794676ae82"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/external-triage.md","bytes":385,"digest":"sha256:4e6b2cfafd491161e7666b304ce0d2d6b19ad705613a6471999c84fda1d63e6a"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/frontend.md","bytes":825,"digest":"sha256:3cd5cba8160ea9f6419ffe82920f7641d09eb7126a78ab50606dfe045732cc02"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/issue-tracker.md","bytes":353,"digest":"sha256:f2139d498fb79358a240d8c99ab1718764217cf28aec0fb024beadfa2b0231cc"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/monorepo.md","bytes":371,"digest":"sha256:47cf6a0ac72c2bcacfc69e5566b1cdcc2102e552b1d91592e5cb1033d30be9e3"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/secondbrain.md","bytes":1589,"digest":"sha256:fab2b2896ad03e0436414b83e5cd80e7fdc07b96d9231b798194b36314a8e7a7"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/skill-dispatch.md","bytes":14414,"digest":"sha256:d424a3e360031e39913bf385f87012e6d1ab79a84024701092859640d2fd05dd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/spec-routing.md","bytes":2269,"digest":"sha256:ce1b67f65ce8ad8a5dc1505e2201833d0e477ff91a6161c395db1188ae83ce9f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/typescript-bun.md","bytes":1378,"digest":"sha256:ec114298fee6ef89bbf0423a6b8a467630296264fc49e989d89af3301093b96e"},{"path":"lock-hash-compatibility-v1.json","bytes":366,"digest":"sha256:4020da4a35468cafb9091dad688be99f4bec87385be348a37d06d2e5192e60ec"},{"path":"modules/autonomous-work.json","bytes":2400,"digest":"sha256:61cb4100b7a9378f4d582964d6ac7fac52e005f5b27d0e5c359bb80a9fce7f85"},{"path":"modules/backend.json","bytes":5655,"digest":"sha256:c5b3d012e5ee3ec4dabcb5ea679fbc89932eb9760215c0c91d0bc8bb45a4f806"},{"path":"modules/bun.json","bytes":2557,"digest":"sha256:ca869344629ff4ede015e761c87afb01c20c371c05260cef56f36a7376e53777"},{"path":"modules/cli-surface.json","bytes":1721,"digest":"sha256:04e84d41b65452dc06d43ce43e2aedec3cc05d100d18aacc150f808ae80f7d32"},{"path":"modules/context-workflow.json","bytes":11142,"digest":"sha256:fb61822b2ce29891d6a06963b225a4da6935859df83ae1aa3fde9fa3bf0b0d30"},{"path":"modules/core.json","bytes":18107,"digest":"sha256:69dd99c698a50f651b0bd0b3021b2abbfba645955fe534735e97bff4ef0f1098"},{"path":"modules/external-triage.json","bytes":1159,"digest":"sha256:5ea90217b042667a3e00211941d4cb1bafb24294187635bfa277765c05894366"},{"path":"modules/frontend.json","bytes":8713,"digest":"sha256:6a701d874b2217bb7f59cddfdd0d0d8a8bb41c6452fc63b67251e5beddc25f7d"},{"path":"modules/go.json","bytes":3322,"digest":"sha256:d9877c86f59206b3bf6bdc1bf73fdfd18fdcc496bc67770413963587276b0533"},{"path":"modules/monorepo.json","bytes":1385,"digest":"sha256:e6485fd6dacb765a866dd9ff79f057d2ebc85cb57c603774fe7ca453cf50f917"},{"path":"modules/repository-extension.json","bytes":1094,"digest":"sha256:a2adeac11000706839944c2ecca225e524da6f38534e54efc9cb0a26f5413f47"},{"path":"modules/rust.json","bytes":2328,"digest":"sha256:46537524b19a5eb1b61dd07d7215b2ea607c1b78a481a21eb13926d40ecdc0b2"},{"path":"modules/secondbrain.json","bytes":4333,"digest":"sha256:3ce59d0ddfcd0e2e5de34d9a4541a27bc62c6e72800da1cd02353ba2f2aa2024"},{"path":"modules/spec-workflow.json","bytes":6441,"digest":"sha256:574f08d79b15e4d9a6a38c3a9a85636f5a208aa9e43051383815056fc03c6e53"},{"path":"modules/tui-surface.json","bytes":1658,"digest":"sha256:06e18ef65d3a7184aaa3a80a21d2279b3376ab77f07bb8b47d23497b0fb6fa62"},{"path":"modules/typescript.json","bytes":8526,"digest":"sha256:7a2a82f7efb28df7dbc343f1b5a7b0ab176e8a74105eb6077c7b5cf6648bdbc6"},{"path":"profiles/go-cli-tui.json","bytes":1709,"digest":"sha256:231220ac521a8298d86440f6e262d61b9c2fa249459dd9206973d8db6e6941bb"},{"path":"profiles/rust-cli.json","bytes":1627,"digest":"sha256:f2338dbf8d0085a6150293c157b6986714de122b2ebd732e33a09f0614686c81"},{"path":"profiles/standard-typescript-monorepo.json","bytes":10907,"digest":"sha256:803f62fb451f9fda23a58b7bfe5f5367a45d8d44bb63c3f96e33b259393cc5ba"},{"path":"retention/transition.legacy-typescript-bun-to-portable-v3.json","bytes":12425,"digest":"sha256:3b166a42eb1f9f23202afc48399eb7aa9b0e8563d9c40095ad6bf710a5d1c599"},{"path":"retention/transition.managed-v2-to-portable-v3.json","bytes":11185,"digest":"sha256:8a0e9dd06aa6a5988c010f6c1286c39002c65e5aae6762e9af6956424524c3e9"},{"path":"setups/go-cli.json","bytes":13810,"digest":"sha256:f9363a094e5c921b7ac3392c0c614571e5e997f8d85780edb18b3c03907745a6"},{"path":"setups/rust-cli.json","bytes":12083,"digest":"sha256:4f482be07ea8e01c3147b6b2d9af04df75cc4ac30a002d2e34dff0aa8c1c0226"},{"path":"setups/typescript-bun.json","bytes":40649,"digest":"sha256:e8db0c1fa8c7d6eb3982ac5e672361b774011dad95db9432331f76f270a52615"},{"path":"skill-activations.json","bytes":3330,"digest":"sha256:bbf1978828a2d383c9cee675a612c17281224e0a1ce5df4b09ba13af4ccd3667"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/accounting.json","bytes":17193,"digest":"sha256:69e39f9df0eb54d08ecb739dc3dfc9dd80c0cf76016aca3d6a0566e48cfaa1f5"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/baseline.json","bytes":498,"digest":"sha256:dca6a12ae24a77fc9104dfcccb507c50dee0e5257e0bf0bd9d86c0b04ce7ffe0"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/AGENTS.md","bytes":583,"digest":"sha256:3b6b36fd045ecb683aec29e0ceac52b3704604815d8035297bfeb8438602494e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/agent-instructions.md","bytes":8996,"digest":"sha256:fe584b8901ae2521dcba7014ca11f3557109a4d91d7c56a21852f027a4bf205e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/autonomous-work.md","bytes":1548,"digest":"sha256:3deff187dba70156ca834529741c4be7df84c5a66f8462ba01594fee35d40336"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/backend.md","bytes":2197,"digest":"sha256:c553c12e3cb13b0435a219fd52ac735bf4f9f3aad05fdb943fd6922cef0a97ac"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/docs-layout.md","bytes":7671,"digest":"sha256:8a78a3543a0a52ac93ef83ab36f953e813be068e96957553483c928cbad439bd"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/domain.md","bytes":947,"digest":"sha256:54c79c324d20b412c07c634f490ad18eeace0562caffe5507b3c83cebf9aaf20"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/external-triage.md","bytes":335,"digest":"sha256:0ec2089110c6a673f305e86f436538c7c07aeef46d29a4e68674defb8778e2cc"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/frontend.md","bytes":1934,"digest":"sha256:4a3609c27bf100aaf0d7de934ad43c0f6a561a15600517422a325fcc6120bd14"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/issue-tracker.md","bytes":890,"digest":"sha256:d86ab246d701a54b8d61c5dcdc1d29fe3281115c028e8694f10574ce99e537c7"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/monorepo.md","bytes":366,"digest":"sha256:c59091add143e67b3773df78d95cc4c8f11d43909655ef1c09dd64c0cc903b3d"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/secondbrain.md","bytes":3312,"digest":"sha256:4b58f3ed96f654d8b99ccc9a03718505f4c43a6170eceef47e26484b5f95bffb"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/skill-dispatch.md","bytes":374,"digest":"sha256:90bbf02e35bba9c707e4713eef4de9a6db36ba3fa93393531287aaf94fce8f8b"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/spec-routing.md","bytes":3094,"digest":"sha256:dfc867401e547d4370f20c578a22e6b82cc80d7b677072f31bc11cdba4851085"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/typescript-bun.md","bytes":2561,"digest":"sha256:7295fe2e544af37e6d6777537fa7ee512b78f3da932d736cc7eabc8d190a30e8"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/manifest.json","bytes":37175,"digest":"sha256:e9a9e91f8dbbea8dc416a03fe813b9c370a2e012118f548b18f1c6a13b6b7662"},{"path":"source-baselines/index.json","bytes":5266,"digest":"sha256:ad636b4cbaf57e27c81093aee9963bcd2b276df7ac243f04b423bf989db314a8"},{"path":"templates/extensions/specific-repository.md","bytes":38,"digest":"sha256:f8aad350dfecd91923c12fe215c1e21dd6741635e44869110391ba9c5c89d137"},{"path":"templates/guides/agent-instructions.md","bytes":260,"digest":"sha256:91f0a4b1d664cf6ab261499b3f7cb093bd2337d6567d07599faebd8fe58dc8d3"},{"path":"templates/guides/autonomous-work.md","bytes":194,"digest":"sha256:384fdebdb4ffd5d6c1b7b197cc02e3f9d3e7327acd7151dc7ce29c0e53e99486"},{"path":"templates/guides/backend.md","bytes":201,"digest":"sha256:0bffbf1e74c9b8c506ec1c612aac0dfaedcfb6f1b7aafc8d1a2d6029cd1170ea"},{"path":"templates/guides/bun.md","bytes":26,"digest":"sha256:3a0640850dc84c8dd6c6b35f10864ae6d3d9c153147699b7bfced6332315633d"},{"path":"templates/guides/cli-surface.md","bytes":34,"digest":"sha256:7cdd6ecc6575c4ef165858ced6409278430a89e3f67baa4281151dc13e2742bc"},{"path":"templates/guides/docs-layout.md","bytes":34,"digest":"sha256:51e9033d80e6742321b47b19c85034c9b4d15b36b9c84230512e3a9a4ef90628"},{"path":"templates/guides/domain-multi-context.md","bytes":112,"digest":"sha256:56b7000576757f37da275123ab75ff5f84d2e26bd88c9f67034a24f3d4883a41"},{"path":"templates/guides/domain-single-context.md","bytes":104,"digest":"sha256:f75666666408761a67d23855f5f78e4526328404343f09a036f8987895c69c0d"},{"path":"templates/guides/domain.md","bytes":59,"digest":"sha256:5c4195e0348b6e5a57679fc833e58b09f4b08080b1cec90a946d0b666310739e"},{"path":"templates/guides/external-triage.md","bytes":38,"digest":"sha256:f26c4164ff69bf44263d9993e4209a9305b8979f228f6f519f139e83f9b51ae9"},{"path":"templates/guides/frontend.md","bytes":265,"digest":"sha256:3243626ac7b55130ac198ba32a350966a26f6a9801a4a4fee259817b2f7eb5f9"},{"path":"templates/guides/go.md","bytes":25,"digest":"sha256:7f597711bdab9fe85ecda9f5895687e436009446ee8416ed96f7bc27fe7b00fd"},{"path":"templates/guides/issue-tracker.md","bytes":36,"digest":"sha256:48840599c91f2f3cf2fd79700f41e54bc3edbb4fa19e4861247e19bac0077fb1"},{"path":"templates/guides/monorepo.md","bytes":31,"digest":"sha256:e1866f1633f3bbc5b230b5c5290b71ae47110dce4d4344c264a9eb325a477bca"},{"path":"templates/guides/rust.md","bytes":27,"digest":"sha256:e0e693135266e96cabec918b14f0cf51c7bf1a126d900001cc418f982f8bbec2"},{"path":"templates/guides/secondbrain.md","bytes":34,"digest":"sha256:a59dc0ae149d4bb3bdd633f79ba91fd17e6d3d3d930c346e5757b5f85ab69002"},{"path":"templates/guides/skill-dispatch.md","bytes":253,"digest":"sha256:0f054b846da5b17409dde049b3b182489e6c24e84734d4a5ffc47a9f504e8a7c"},{"path":"templates/guides/spec-docs-layout.md","bytes":39,"digest":"sha256:6d9ddb174c4f69cb1f9abe428a6be677e9ea8dbecc19ae0fa2f53fdc3749de40"},{"path":"templates/guides/spec-routing.md","bytes":35,"digest":"sha256:cc578e73968c15bdcaad35270454e91462b4413d937cfd984404ef22d765482c"},{"path":"templates/guides/tui-surface.md","bytes":34,"digest":"sha256:bbc422cb32df905d526a291cbfd5b81fd86f0d405f5f650cc5d8eb0a864c4eb2"},{"path":"templates/guides/typescript-bun.md","bytes":41,"digest":"sha256:ee14609660ebe1da17eff0d12c3f42857cba4c92be0f723346d17e3d64bb6574"},{"path":"templates/index.json","bytes":7125,"digest":"sha256:272cbfd4fa2ea380acfc76ce4d75eaf2ae026b7501c34ee48a8645644eb72f4c"},{"path":"templates/root/autonomous-work.md","bytes":102,"digest":"sha256:abd0be4c74295597be5dca6a307e41d6bdcfa5113370f5bbc30d30725c927e35"},{"path":"templates/root/backend.md","bytes":76,"digest":"sha256:f65a96aaa0b79d40d209df159c30bd6ac791e36c415b8ba2dc52a1032c5785c3"},{"path":"templates/root/bun.md","bytes":89,"digest":"sha256:48a4bcd7c9b86d8f3be6965ff8b7b33aad77148aa598d49ed8efa74acd3c8e20"},{"path":"templates/root/cli-surface.md","bytes":79,"digest":"sha256:ec022b0b1767d88ebc849f2ee7d8a2d266a36d419f8084b9cce8599d0619a125"},{"path":"templates/root/context-workflow.md","bytes":129,"digest":"sha256:52c53e99fed6c5d59388e58f7854c96f8130da553d80bb90f04935f67eea45c5"},{"path":"templates/root/core.md","bytes":213,"digest":"sha256:ffa6f25825e1d5c5bf52373022d7035b004fd78dd55f99a187d89c4b78ddeabb"},{"path":"templates/root/external-triage.md","bytes":91,"digest":"sha256:5ea18c6c7aea8f03d62af0b824b947ddecbaf861fbf9b81df2e8fb2b2c0e2c54"},{"path":"templates/root/frontend.md","bytes":99,"digest":"sha256:cddad0b38dff9cbeae571dc8514f5d2770e528725f8931272389bd8a675e08bb"},{"path":"templates/root/go.md","bytes":67,"digest":"sha256:aa3eafd5ebcec7aa64ae7fddf1c5b954512e831121934760f615c12ec52f6527"},{"path":"templates/root/monorepo.md","bytes":74,"digest":"sha256:76951b431357333d5b3bbbe718529797834c5de14ee6cd886bce175b5ef892e9"},{"path":"templates/root/repository-extension.md","bytes":103,"digest":"sha256:f24b6015a05eab4cdbb830eb485f9958b659d9a99d46b53d8538a813f6705fe1"},{"path":"templates/root/rust.md","bytes":73,"digest":"sha256:40c4f50d3a020406816612b9f9e0c6cbd545626401e447d4963ba2d30d8e40e3"},{"path":"templates/root/secondbrain.md","bytes":87,"digest":"sha256:dbb11588c2dbca5fd803eccc889586cac75824e9083c570bd1cad34fda901857"},{"path":"templates/root/spec-workflow.md","bytes":125,"digest":"sha256:c292c5b2b98facb56ef0f6da0a61f38544377ddb6339c63d5287ca7dec4f2360"},{"path":"templates/root/tui-surface.md","bytes":75,"digest":"sha256:5657d3abaddf480a535d00a8bfe94bcd3e0b6230aaaa3aac2f9ce89fdc7e20b0"},{"path":"templates/root/typescript.md","bytes":76,"digest":"sha256:9673f69acb604560153c93c46aa6a8a03ecedc81c3eae15e03b0b1b763dbcde7"}],"profiles":["go-cli-tui","rust-cli","standard-typescript-monorepo"],"modules":["autonomous-work","backend","bun","cli-surface","context-workflow","core","external-triage","frontend","go","monorepo","repository-extension","rust","secondbrain","spec-workflow","tui-surface","typescript"],"decisions":["auth.provider","autonomous.enabled","domain.layout","http.contract","identifier.strategy","language.generated","repository.extension.enabled","runtime.backend","runtime.design","secondbrain.enabled","spec.scaffold","triage.external","verification.gate"],"templates":["template.extension.repository-rules","template.guide.agent-instructions","template.guide.autonomous-work","template.guide.backend","template.guide.bun","template.guide.cli-surface","template.guide.docs-layout","template.guide.domain","template.guide.domain.multi-context","template.guide.domain.single-context","template.guide.external-triage","template.guide.frontend","template.guide.go","template.guide.issue-tracker","template.guide.monorepo","template.guide.rust","template.guide.secondbrain","template.guide.skill-dispatch","template.guide.spec-docs-layout","template.guide.spec-routing","template.guide.tui-surface","template.guide.typescript-bun","template.root.autonomous-work","template.root.backend","template.root.bun","template.root.cli-surface","template.root.context-workflow","template.root.core","template.root.external-triage","template.root.frontend","template.root.go","template.root.monorepo","template.root.repository-extension","template.root.rust","template.root.secondbrain","template.root.spec-workflow","template.root.tui-surface","template.root.typescript"],"setups":["go-cli","rust-cli","typescript-bun"],"retentionTransitions":["transition.legacy-typescript-bun-to-portable-v3","transition.managed-v2-to-portable-v3"]} +{"schemaVersion":"roundfix/baseline-catalog/v1","files":[{"path":"contract-v1.json","bytes":13525,"digest":"sha256:0c32b7ac1b6dd1a2a04d830b300d592b7b195aec7bb32ff05575e8d19f154497"},{"path":"coverage.json","bytes":4660,"digest":"sha256:9faa73bf86fad383570b0098a9aa26185ea8d689f9866038ab37b8a34b9a01dc"},{"path":"decisions.json","bytes":8438,"digest":"sha256:58acbb05cf46057923e8844b28e2a97efa898d6200ae9e0773971b6287b05651"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/AGENTS.md","bytes":2911,"digest":"sha256:1876cc64ced89e60cfb0188eaff71d8b588c80d1149740ba368d1afc969e1d8f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/agent-instructions.md","bytes":4182,"digest":"sha256:eb769f99afa32a901723e1962a06fca09d856c8d37284b36c9aed071bd6e4f86"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/autonomous-work.md","bytes":789,"digest":"sha256:76065b3f1b452f647765d01bfb802ca3a0757d7212ba75b8e55d1d6242b0c977"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/backend.md","bytes":1083,"digest":"sha256:b8459f66c5fec1d424768fdd2b0b2f4ec70b9038208c1d5a58086720586195fd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/docs-layout.md","bytes":3546,"digest":"sha256:a188cfaae89ed195ca31460af3ec5d0dfad0bc1241e1958950ed4e0035de86f6"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/domain.md","bytes":614,"digest":"sha256:089a408493f450ef5713f3f62b20b51323b6d08617e6e43a12e38d794676ae82"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/external-triage.md","bytes":385,"digest":"sha256:4e6b2cfafd491161e7666b304ce0d2d6b19ad705613a6471999c84fda1d63e6a"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/frontend.md","bytes":825,"digest":"sha256:3cd5cba8160ea9f6419ffe82920f7641d09eb7126a78ab50606dfe045732cc02"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/issue-tracker.md","bytes":353,"digest":"sha256:f2139d498fb79358a240d8c99ab1718764217cf28aec0fb024beadfa2b0231cc"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/monorepo.md","bytes":371,"digest":"sha256:47cf6a0ac72c2bcacfc69e5566b1cdcc2102e552b1d91592e5cb1033d30be9e3"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/secondbrain.md","bytes":1589,"digest":"sha256:fab2b2896ad03e0436414b83e5cd80e7fdc07b96d9231b798194b36314a8e7a7"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/skill-dispatch.md","bytes":14414,"digest":"sha256:d424a3e360031e39913bf385f87012e6d1ab79a84024701092859640d2fd05dd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/spec-routing.md","bytes":2269,"digest":"sha256:ce1b67f65ce8ad8a5dc1505e2201833d0e477ff91a6161c395db1188ae83ce9f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/typescript-bun.md","bytes":1378,"digest":"sha256:ec114298fee6ef89bbf0423a6b8a467630296264fc49e989d89af3301093b96e"},{"path":"lock-hash-compatibility-v1.json","bytes":366,"digest":"sha256:4020da4a35468cafb9091dad688be99f4bec87385be348a37d06d2e5192e60ec"},{"path":"modules/autonomous-work.json","bytes":2400,"digest":"sha256:61cb4100b7a9378f4d582964d6ac7fac52e005f5b27d0e5c359bb80a9fce7f85"},{"path":"modules/backend.json","bytes":5655,"digest":"sha256:c5b3d012e5ee3ec4dabcb5ea679fbc89932eb9760215c0c91d0bc8bb45a4f806"},{"path":"modules/bun.json","bytes":2557,"digest":"sha256:ca869344629ff4ede015e761c87afb01c20c371c05260cef56f36a7376e53777"},{"path":"modules/cli-surface.json","bytes":1721,"digest":"sha256:04e84d41b65452dc06d43ce43e2aedec3cc05d100d18aacc150f808ae80f7d32"},{"path":"modules/context-workflow.json","bytes":11142,"digest":"sha256:fb61822b2ce29891d6a06963b225a4da6935859df83ae1aa3fde9fa3bf0b0d30"},{"path":"modules/core.json","bytes":18107,"digest":"sha256:69dd99c698a50f651b0bd0b3021b2abbfba645955fe534735e97bff4ef0f1098"},{"path":"modules/external-triage.json","bytes":1159,"digest":"sha256:5ea90217b042667a3e00211941d4cb1bafb24294187635bfa277765c05894366"},{"path":"modules/frontend.json","bytes":8713,"digest":"sha256:6a701d874b2217bb7f59cddfdd0d0d8a8bb41c6452fc63b67251e5beddc25f7d"},{"path":"modules/go.json","bytes":3322,"digest":"sha256:d9877c86f59206b3bf6bdc1bf73fdfd18fdcc496bc67770413963587276b0533"},{"path":"modules/monorepo.json","bytes":1385,"digest":"sha256:e6485fd6dacb765a866dd9ff79f057d2ebc85cb57c603774fe7ca453cf50f917"},{"path":"modules/repository-extension.json","bytes":1094,"digest":"sha256:a2adeac11000706839944c2ecca225e524da6f38534e54efc9cb0a26f5413f47"},{"path":"modules/rust.json","bytes":2328,"digest":"sha256:46537524b19a5eb1b61dd07d7215b2ea607c1b78a481a21eb13926d40ecdc0b2"},{"path":"modules/secondbrain.json","bytes":4333,"digest":"sha256:3ce59d0ddfcd0e2e5de34d9a4541a27bc62c6e72800da1cd02353ba2f2aa2024"},{"path":"modules/spec-workflow.json","bytes":6441,"digest":"sha256:574f08d79b15e4d9a6a38c3a9a85636f5a208aa9e43051383815056fc03c6e53"},{"path":"modules/tui-surface.json","bytes":1658,"digest":"sha256:06e18ef65d3a7184aaa3a80a21d2279b3376ab77f07bb8b47d23497b0fb6fa62"},{"path":"modules/typescript.json","bytes":8526,"digest":"sha256:7a2a82f7efb28df7dbc343f1b5a7b0ab176e8a74105eb6077c7b5cf6648bdbc6"},{"path":"profiles/go-cli-tui.json","bytes":1709,"digest":"sha256:231220ac521a8298d86440f6e262d61b9c2fa249459dd9206973d8db6e6941bb"},{"path":"profiles/rust-cli.json","bytes":1627,"digest":"sha256:f2338dbf8d0085a6150293c157b6986714de122b2ebd732e33a09f0614686c81"},{"path":"profiles/standard-typescript-monorepo.json","bytes":10907,"digest":"sha256:803f62fb451f9fda23a58b7bfe5f5367a45d8d44bb63c3f96e33b259393cc5ba"},{"path":"retention/transition.legacy-typescript-bun-to-portable-v3.json","bytes":12425,"digest":"sha256:3b166a42eb1f9f23202afc48399eb7aa9b0e8563d9c40095ad6bf710a5d1c599"},{"path":"retention/transition.managed-v2-to-portable-v3.json","bytes":11185,"digest":"sha256:8a0e9dd06aa6a5988c010f6c1286c39002c65e5aae6762e9af6956424524c3e9"},{"path":"setups/go-cli.json","bytes":14203,"digest":"sha256:28e2ad53684c2c097affa4cf70c32a59265435dd105cdcfa57314bd18a6af46f"},{"path":"setups/rust-cli.json","bytes":12476,"digest":"sha256:82cdba03b2c77c9a0c4a6ad737e7de5e90a3fcf6827c18242793b8032766d9c0"},{"path":"setups/typescript-bun.json","bytes":40649,"digest":"sha256:573437aeb58bf8ef7cdd2c37d250b6ab3c66d93d12a69e8d388b4644862ffb3f"},{"path":"skill-activations.json","bytes":3330,"digest":"sha256:bbf1978828a2d383c9cee675a612c17281224e0a1ce5df4b09ba13af4ccd3667"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/accounting.json","bytes":17193,"digest":"sha256:69e39f9df0eb54d08ecb739dc3dfc9dd80c0cf76016aca3d6a0566e48cfaa1f5"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/baseline.json","bytes":498,"digest":"sha256:dca6a12ae24a77fc9104dfcccb507c50dee0e5257e0bf0bd9d86c0b04ce7ffe0"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/AGENTS.md","bytes":583,"digest":"sha256:3b6b36fd045ecb683aec29e0ceac52b3704604815d8035297bfeb8438602494e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/agent-instructions.md","bytes":8996,"digest":"sha256:fe584b8901ae2521dcba7014ca11f3557109a4d91d7c56a21852f027a4bf205e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/autonomous-work.md","bytes":1548,"digest":"sha256:3deff187dba70156ca834529741c4be7df84c5a66f8462ba01594fee35d40336"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/backend.md","bytes":2197,"digest":"sha256:c553c12e3cb13b0435a219fd52ac735bf4f9f3aad05fdb943fd6922cef0a97ac"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/docs-layout.md","bytes":7671,"digest":"sha256:8a78a3543a0a52ac93ef83ab36f953e813be068e96957553483c928cbad439bd"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/domain.md","bytes":947,"digest":"sha256:54c79c324d20b412c07c634f490ad18eeace0562caffe5507b3c83cebf9aaf20"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/external-triage.md","bytes":335,"digest":"sha256:0ec2089110c6a673f305e86f436538c7c07aeef46d29a4e68674defb8778e2cc"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/frontend.md","bytes":1934,"digest":"sha256:4a3609c27bf100aaf0d7de934ad43c0f6a561a15600517422a325fcc6120bd14"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/issue-tracker.md","bytes":890,"digest":"sha256:d86ab246d701a54b8d61c5dcdc1d29fe3281115c028e8694f10574ce99e537c7"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/monorepo.md","bytes":366,"digest":"sha256:c59091add143e67b3773df78d95cc4c8f11d43909655ef1c09dd64c0cc903b3d"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/secondbrain.md","bytes":3312,"digest":"sha256:4b58f3ed96f654d8b99ccc9a03718505f4c43a6170eceef47e26484b5f95bffb"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/skill-dispatch.md","bytes":374,"digest":"sha256:90bbf02e35bba9c707e4713eef4de9a6db36ba3fa93393531287aaf94fce8f8b"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/spec-routing.md","bytes":3094,"digest":"sha256:dfc867401e547d4370f20c578a22e6b82cc80d7b677072f31bc11cdba4851085"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/typescript-bun.md","bytes":2561,"digest":"sha256:7295fe2e544af37e6d6777537fa7ee512b78f3da932d736cc7eabc8d190a30e8"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/manifest.json","bytes":37175,"digest":"sha256:e9a9e91f8dbbea8dc416a03fe813b9c370a2e012118f548b18f1c6a13b6b7662"},{"path":"source-baselines/index.json","bytes":5266,"digest":"sha256:ad636b4cbaf57e27c81093aee9963bcd2b276df7ac243f04b423bf989db314a8"},{"path":"templates/extensions/specific-repository.md","bytes":38,"digest":"sha256:f8aad350dfecd91923c12fe215c1e21dd6741635e44869110391ba9c5c89d137"},{"path":"templates/guides/agent-instructions.md","bytes":260,"digest":"sha256:91f0a4b1d664cf6ab261499b3f7cb093bd2337d6567d07599faebd8fe58dc8d3"},{"path":"templates/guides/autonomous-work.md","bytes":194,"digest":"sha256:384fdebdb4ffd5d6c1b7b197cc02e3f9d3e7327acd7151dc7ce29c0e53e99486"},{"path":"templates/guides/backend.md","bytes":201,"digest":"sha256:0bffbf1e74c9b8c506ec1c612aac0dfaedcfb6f1b7aafc8d1a2d6029cd1170ea"},{"path":"templates/guides/bun.md","bytes":26,"digest":"sha256:3a0640850dc84c8dd6c6b35f10864ae6d3d9c153147699b7bfced6332315633d"},{"path":"templates/guides/cli-surface.md","bytes":34,"digest":"sha256:7cdd6ecc6575c4ef165858ced6409278430a89e3f67baa4281151dc13e2742bc"},{"path":"templates/guides/docs-layout.md","bytes":34,"digest":"sha256:51e9033d80e6742321b47b19c85034c9b4d15b36b9c84230512e3a9a4ef90628"},{"path":"templates/guides/domain-multi-context.md","bytes":112,"digest":"sha256:56b7000576757f37da275123ab75ff5f84d2e26bd88c9f67034a24f3d4883a41"},{"path":"templates/guides/domain-single-context.md","bytes":104,"digest":"sha256:f75666666408761a67d23855f5f78e4526328404343f09a036f8987895c69c0d"},{"path":"templates/guides/domain.md","bytes":59,"digest":"sha256:5c4195e0348b6e5a57679fc833e58b09f4b08080b1cec90a946d0b666310739e"},{"path":"templates/guides/external-triage.md","bytes":38,"digest":"sha256:f26c4164ff69bf44263d9993e4209a9305b8979f228f6f519f139e83f9b51ae9"},{"path":"templates/guides/frontend.md","bytes":265,"digest":"sha256:3243626ac7b55130ac198ba32a350966a26f6a9801a4a4fee259817b2f7eb5f9"},{"path":"templates/guides/go.md","bytes":25,"digest":"sha256:7f597711bdab9fe85ecda9f5895687e436009446ee8416ed96f7bc27fe7b00fd"},{"path":"templates/guides/issue-tracker.md","bytes":36,"digest":"sha256:48840599c91f2f3cf2fd79700f41e54bc3edbb4fa19e4861247e19bac0077fb1"},{"path":"templates/guides/monorepo.md","bytes":31,"digest":"sha256:e1866f1633f3bbc5b230b5c5290b71ae47110dce4d4344c264a9eb325a477bca"},{"path":"templates/guides/rust.md","bytes":27,"digest":"sha256:e0e693135266e96cabec918b14f0cf51c7bf1a126d900001cc418f982f8bbec2"},{"path":"templates/guides/secondbrain.md","bytes":34,"digest":"sha256:a59dc0ae149d4bb3bdd633f79ba91fd17e6d3d3d930c346e5757b5f85ab69002"},{"path":"templates/guides/skill-dispatch.md","bytes":253,"digest":"sha256:0f054b846da5b17409dde049b3b182489e6c24e84734d4a5ffc47a9f504e8a7c"},{"path":"templates/guides/spec-docs-layout.md","bytes":39,"digest":"sha256:6d9ddb174c4f69cb1f9abe428a6be677e9ea8dbecc19ae0fa2f53fdc3749de40"},{"path":"templates/guides/spec-routing.md","bytes":35,"digest":"sha256:cc578e73968c15bdcaad35270454e91462b4413d937cfd984404ef22d765482c"},{"path":"templates/guides/tui-surface.md","bytes":34,"digest":"sha256:bbc422cb32df905d526a291cbfd5b81fd86f0d405f5f650cc5d8eb0a864c4eb2"},{"path":"templates/guides/typescript-bun.md","bytes":41,"digest":"sha256:ee14609660ebe1da17eff0d12c3f42857cba4c92be0f723346d17e3d64bb6574"},{"path":"templates/index.json","bytes":7125,"digest":"sha256:272cbfd4fa2ea380acfc76ce4d75eaf2ae026b7501c34ee48a8645644eb72f4c"},{"path":"templates/root/autonomous-work.md","bytes":102,"digest":"sha256:abd0be4c74295597be5dca6a307e41d6bdcfa5113370f5bbc30d30725c927e35"},{"path":"templates/root/backend.md","bytes":76,"digest":"sha256:f65a96aaa0b79d40d209df159c30bd6ac791e36c415b8ba2dc52a1032c5785c3"},{"path":"templates/root/bun.md","bytes":89,"digest":"sha256:48a4bcd7c9b86d8f3be6965ff8b7b33aad77148aa598d49ed8efa74acd3c8e20"},{"path":"templates/root/cli-surface.md","bytes":79,"digest":"sha256:ec022b0b1767d88ebc849f2ee7d8a2d266a36d419f8084b9cce8599d0619a125"},{"path":"templates/root/context-workflow.md","bytes":129,"digest":"sha256:52c53e99fed6c5d59388e58f7854c96f8130da553d80bb90f04935f67eea45c5"},{"path":"templates/root/core.md","bytes":213,"digest":"sha256:ffa6f25825e1d5c5bf52373022d7035b004fd78dd55f99a187d89c4b78ddeabb"},{"path":"templates/root/external-triage.md","bytes":91,"digest":"sha256:5ea18c6c7aea8f03d62af0b824b947ddecbaf861fbf9b81df2e8fb2b2c0e2c54"},{"path":"templates/root/frontend.md","bytes":99,"digest":"sha256:cddad0b38dff9cbeae571dc8514f5d2770e528725f8931272389bd8a675e08bb"},{"path":"templates/root/go.md","bytes":67,"digest":"sha256:aa3eafd5ebcec7aa64ae7fddf1c5b954512e831121934760f615c12ec52f6527"},{"path":"templates/root/monorepo.md","bytes":74,"digest":"sha256:76951b431357333d5b3bbbe718529797834c5de14ee6cd886bce175b5ef892e9"},{"path":"templates/root/repository-extension.md","bytes":103,"digest":"sha256:f24b6015a05eab4cdbb830eb485f9958b659d9a99d46b53d8538a813f6705fe1"},{"path":"templates/root/rust.md","bytes":73,"digest":"sha256:40c4f50d3a020406816612b9f9e0c6cbd545626401e447d4963ba2d30d8e40e3"},{"path":"templates/root/secondbrain.md","bytes":87,"digest":"sha256:dbb11588c2dbca5fd803eccc889586cac75824e9083c570bd1cad34fda901857"},{"path":"templates/root/spec-workflow.md","bytes":125,"digest":"sha256:c292c5b2b98facb56ef0f6da0a61f38544377ddb6339c63d5287ca7dec4f2360"},{"path":"templates/root/tui-surface.md","bytes":75,"digest":"sha256:5657d3abaddf480a535d00a8bfe94bcd3e0b6230aaaa3aac2f9ce89fdc7e20b0"},{"path":"templates/root/typescript.md","bytes":76,"digest":"sha256:9673f69acb604560153c93c46aa6a8a03ecedc81c3eae15e03b0b1b763dbcde7"}],"profiles":["go-cli-tui","rust-cli","standard-typescript-monorepo"],"modules":["autonomous-work","backend","bun","cli-surface","context-workflow","core","external-triage","frontend","go","monorepo","repository-extension","rust","secondbrain","spec-workflow","tui-surface","typescript"],"decisions":["auth.provider","autonomous.enabled","domain.layout","http.contract","identifier.strategy","language.generated","repository.extension.enabled","runtime.backend","runtime.design","secondbrain.enabled","spec.scaffold","triage.external","verification.gate"],"templates":["template.extension.repository-rules","template.guide.agent-instructions","template.guide.autonomous-work","template.guide.backend","template.guide.bun","template.guide.cli-surface","template.guide.docs-layout","template.guide.domain","template.guide.domain.multi-context","template.guide.domain.single-context","template.guide.external-triage","template.guide.frontend","template.guide.go","template.guide.issue-tracker","template.guide.monorepo","template.guide.rust","template.guide.secondbrain","template.guide.skill-dispatch","template.guide.spec-docs-layout","template.guide.spec-routing","template.guide.tui-surface","template.guide.typescript-bun","template.root.autonomous-work","template.root.backend","template.root.bun","template.root.cli-surface","template.root.context-workflow","template.root.core","template.root.external-triage","template.root.frontend","template.root.go","template.root.monorepo","template.root.repository-extension","template.root.rust","template.root.secondbrain","template.root.spec-workflow","template.root.tui-surface","template.root.typescript"],"setups":["go-cli","rust-cli","typescript-bun"],"retentionTransitions":["transition.legacy-typescript-bun-to-portable-v3","transition.managed-v2-to-portable-v3"]} diff --git a/internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json b/internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json index 7f54cf8e..39fb6abc 100644 --- a/internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json +++ b/internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json @@ -22,11 +22,11 @@ }, "managedEntryLedger": [ { - "digest": "16b3e112e61316ce7218945cc281c0cad0d534e1984f1ad68d460cfb608c7dae", + "digest": "e80364c9057d60f384b457607882677bd57a80519fb3390daf0aa57a82007963", "id": "go-cli" }, { - "digest": "275fa22a2d5ffb8f89cdfdc9e3c473c09218f02028b4dc48cc2892eb36b78a22", + "digest": "df7f2640b1d16bec9f2b8ac6e9770b99290aa38706a2f7a87cbd83bb0f4c6156", "id": "rust-cli" }, { @@ -53,7 +53,7 @@ ] } ], - "digest": "16b3e112e61316ce7218945cc281c0cad0d534e1984f1ad68d460cfb608c7dae", + "digest": "e80364c9057d60f384b457607882677bd57a80519fb3390daf0aa57a82007963", "id": "go-cli", "schemaVersion": "setup-context-driven/setup-snapshot/0.0.1", "skills": [ @@ -115,6 +115,17 @@ "type": "repo" } }, + { + "name": "exa-web-search", + "path": "skills/01-discovery/exa-web-search", + "source": { + "path": "skills/01-discovery/exa-web-search", + "ref": "21f59c7c11e5e449abbf2731be43d0a380c68854", + "repository": "example/skills", + "type": "github" + }, + "treeDigest": "01aff779d5fc2e994692d2de6410375e81d4365cd9f65cce2d105d7f405d28ea" + }, { "name": "grill-with-docs", "path": "skills/01-discovery/grill-with-docs", @@ -464,7 +475,7 @@ ] } ], - "digest": "275fa22a2d5ffb8f89cdfdc9e3c473c09218f02028b4dc48cc2892eb36b78a22", + "digest": "df7f2640b1d16bec9f2b8ac6e9770b99290aa38706a2f7a87cbd83bb0f4c6156", "id": "rust-cli", "schemaVersion": "setup-context-driven/setup-snapshot/0.0.1", "skills": [ @@ -526,6 +537,17 @@ "type": "repo" } }, + { + "name": "exa-web-search", + "path": "skills/01-discovery/exa-web-search", + "source": { + "path": "skills/01-discovery/exa-web-search", + "ref": "21f59c7c11e5e449abbf2731be43d0a380c68854", + "repository": "example/skills", + "type": "github" + }, + "treeDigest": "01aff779d5fc2e994692d2de6410375e81d4365cd9f65cce2d105d7f405d28ea" + }, { "name": "grill-with-docs", "path": "skills/01-discovery/grill-with-docs", diff --git a/internal/baseline/testdata/parity-corpus/v1/manifest.json b/internal/baseline/testdata/parity-corpus/v1/manifest.json index 27a23a09..48527020 100644 --- a/internal/baseline/testdata/parity-corpus/v1/manifest.json +++ b/internal/baseline/testdata/parity-corpus/v1/manifest.json @@ -20,9 +20,9 @@ "sha256": "247845849dd03055a28d3f41d2e070ae20df43657c20121e7485a0913be64aa8" }, { - "bytes": 82752, + "bytes": 83662, "path": "fixtures/asset-sync.json", - "sha256": "8f6d61206c7ee9c9f9a06558ff443c466d61c394de3933ea30ae84008eb47e8b" + "sha256": "02f8d026276cee7e5b1073f80a182c0ad671e32312a8742d13ebaec4cc0147c2" }, { "bytes": 330237, diff --git a/internal/baselineacp/analyzer.go b/internal/baselineacp/analyzer.go index cc62a542..9d0dd10a 100644 --- a/internal/baselineacp/analyzer.go +++ b/internal/baselineacp/analyzer.go @@ -26,7 +26,8 @@ type Runtime interface { // Analyzer supervises the fixed preferred and fallback Agent Selections. type Analyzer struct { - runtime Runtime + runtime Runtime + findings []baseline.Finding } func NewAnalyzer(runtime Runtime) *Analyzer { @@ -73,12 +74,20 @@ func (analyzer *Analyzer) Segment( return baseline.RuleSegmentationProposal{}, err } if proofErr != nil { + analyzer.recordSelectionFinding("segmentation", selection, proofErr) if cleanupUnproven(proofErr) { return baseline.ManualRuleSegmentationProposal(snapshot) } continue } proven[index] = exactProofMatches(selection, proof) + if !proven[index] { + analyzer.recordSelectionFinding( + "segmentation", + selection, + errors.New("Exact Agent Selection Proof did not match the requested selection"), + ) + } } for index, selection := range selections { @@ -94,6 +103,7 @@ func (analyzer *Analyzer) Segment( if attemptErr == nil { return proposal, nil } + analyzer.recordProposalFinding("segmentation", selection, attemptErr) if err := ctx.Err(); err != nil { return baseline.RuleSegmentationProposal{}, err } @@ -140,12 +150,20 @@ func (analyzer *Analyzer) Classify( return baseline.ClassificationProposal{}, err } if proofErr != nil { + analyzer.recordSelectionFinding("classification", selection, proofErr) if cleanupUnproven(proofErr) { return baseline.ManualClassificationProposal(snapshot) } continue } proven[index] = exactProofMatches(selection, proof) + if !proven[index] { + analyzer.recordSelectionFinding( + "classification", + selection, + errors.New("Exact Agent Selection Proof did not match the requested selection"), + ) + } } for index, selection := range selections { @@ -156,6 +174,7 @@ func (analyzer *Analyzer) Classify( if attemptErr == nil { return proposal, nil } + analyzer.recordProposalFinding("classification", selection, attemptErr) if err := ctx.Err(); err != nil { return baseline.ClassificationProposal{}, err } @@ -197,12 +216,20 @@ func (analyzer *Analyzer) Revise( return baseline.RevisionProposal{}, err } if proofErr != nil { + analyzer.recordSelectionFinding("revision", selection, proofErr) if cleanupUnproven(proofErr) { return baseline.ManualRevisionProposal(snapshot) } continue } proven[index] = exactProofMatches(selection, proof) + if !proven[index] { + analyzer.recordSelectionFinding( + "revision", + selection, + errors.New("Exact Agent Selection Proof did not match the requested selection"), + ) + } } for index, selection := range selections { if !proven[index] { @@ -212,6 +239,7 @@ func (analyzer *Analyzer) Revise( if attemptErr == nil { return proposal, nil } + analyzer.recordProposalFinding("revision", selection, attemptErr) if err := ctx.Err(); err != nil { return baseline.RevisionProposal{}, err } @@ -222,6 +250,70 @@ func (analyzer *Analyzer) Revise( return baseline.ManualRevisionProposal(snapshot) } +// TakeFindings returns and clears sanitized diagnostics from discarded sealed +// selections and proposals. Proposal output itself is never retained. +func (analyzer *Analyzer) TakeFindings() []baseline.Finding { + if analyzer == nil { + return nil + } + findings := append([]baseline.Finding(nil), analyzer.findings...) + analyzer.findings = nil + return findings +} + +func (analyzer *Analyzer) recordSelectionFinding( + stage string, + selection agent.RuntimeSpec, + err error, +) { + analyzer.recordFinding( + "baseline.semantic."+stage+"-selection.unavailable", + stage+" selection", + selection, + err, + ) +} + +func (analyzer *Analyzer) recordProposalFinding( + stage string, + selection agent.RuntimeSpec, + err error, +) { + analyzer.recordFinding( + "baseline.semantic."+stage+"-proposal.discarded", + stage+" proposal", + selection, + err, + ) +} + +func (analyzer *Analyzer) recordFinding( + code string, + subject string, + selection agent.RuntimeSpec, + err error, +) { + if analyzer == nil || err == nil { + return + } + reason := strings.Join(strings.Fields(err.Error()), " ") + const maxReasonRunes = 512 + runes := []rune(reason) + if len(runes) > maxReasonRunes { + reason = string(runes[:maxReasonRunes]) + "..." + } + analyzer.findings = append(analyzer.findings, baseline.Finding{ + Code: code, + Message: fmt.Sprintf( + "%s %s %s was discarded: %s", + selection.DisplayName, + selection.Model, + subject, + reason, + ), + }) +} + func cleanupUnproven(err error) bool { var sessionErr *agent.AgentSessionCleanupError var directoryErr *privateDirectoryCleanupError diff --git a/internal/baselineacp/analyzer_test.go b/internal/baselineacp/analyzer_test.go index 4d9da76d..cd11c8ff 100644 --- a/internal/baselineacp/analyzer_test.go +++ b/internal/baselineacp/analyzer_test.go @@ -56,10 +56,18 @@ func TestRuleSegmentationPreferredFallbackUsesIdenticalSnapshotBytes(t *testing. }, } - proposal, err := NewAnalyzer(runtime).Segment(context.Background(), snapshot) + analyzer := NewAnalyzer(runtime) + proposal, err := analyzer.Segment(context.Background(), snapshot) if err != nil { t.Fatalf("segment with fallback selection: %v", err) } + findings := analyzer.TakeFindings() + if len(findings) != 1 || + findings[0].Code != "baseline.semantic.segmentation-proposal.discarded" || + !strings.Contains(findings[0].Message, PreferredModel) || + !strings.Contains(findings[0].Message, "parse Segmentation Proposal") { + t.Fatalf("discarded preferred proposal finding = %+v", findings) + } materialized, err := baseline.MaterializeRuleSegments(snapshot, proposal) if err != nil { t.Fatalf("materialize fallback proposal: %v", err) @@ -417,8 +425,8 @@ func TestACPXReadOnlyArguments(t *testing.T) { "--allowed-tools\x00", "--max-turns\x001", "--no-terminal", - "--timeout\x00120", - "--ttl\x00120", + "--timeout\x00300", + "--ttl\x00300", } { if !strings.Contains(joined, required) { t.Fatalf("%s args omit sealed flag %q: %#v", name, required, args) diff --git a/internal/cli/baseline_human.go b/internal/cli/baseline_human.go index 725f8601..9ef019d1 100644 --- a/internal/cli/baseline_human.go +++ b/internal/cli/baseline_human.go @@ -43,6 +43,10 @@ type baselineSemanticAnalyzer interface { Classify(context.Context, baseline.AnalysisSnapshot) (baseline.ClassificationProposal, error) } +type baselineSemanticFindingSource interface { + TakeFindings() []baseline.Finding +} + type baselineHumanPrompt struct { reader *bufio.Reader writer io.Writer @@ -1562,6 +1566,7 @@ func promptBaselineClassification( err, ) } + writeBaselineSemanticFindings(review, analyzer) source, err := baseline.MaterializeRuleSegments(segmentationSnapshot, segmentationProposal) if err != nil { return baseline.RootPreservationRequest{}, fmt.Errorf( @@ -1592,6 +1597,7 @@ func promptBaselineClassification( err, ) } + writeBaselineSemanticFindings(review, analyzer) document, err := baseline.DecisionDocumentFromClassificationProposal( analysisSnapshot, classificationProposal, @@ -1707,6 +1713,19 @@ func promptBaselineClassification( return request, nil } +func writeBaselineSemanticFindings( + review io.Writer, + analyzer baselineSemanticAnalyzer, +) { + source, ok := analyzer.(baselineSemanticFindingSource) + if !ok { + return + } + for _, finding := range source.TakeFindings() { + fmt.Fprintf(review, "Warning: %s: %s\n", finding.Code, finding.Message) + } +} + type baselineClassificationChoice struct { label string disposition string diff --git a/internal/cli/baseline_human_test.go b/internal/cli/baseline_human_test.go index 6fb65adf..6f43ad88 100644 --- a/internal/cli/baseline_human_test.go +++ b/internal/cli/baseline_human_test.go @@ -793,6 +793,14 @@ func TestHumanBaselineInvokesSemanticSegmentationAndClassification(t *testing.T) analyzer.classifyCalls, ) } + for _, want := range []string{ + "baseline.semantic.segmentation-proposal.discarded", + "baseline.semantic.classification-proposal.discarded", + } { + if !strings.Contains(stdout.String(), want) { + t.Fatalf("semantic analyzer finding %q is absent:\n%s", want, stdout.String()) + } + } if !strings.Contains(stdout.String(), "normative-clause -> repository-document") { t.Fatalf("semantic repository-document proposal is absent:\n%s", stdout.String()) } @@ -1395,6 +1403,7 @@ func (analyzer *countingBaselineRevisionAnalyzer) Revise( type countingBaselineSemanticAnalyzer struct { segmentCalls int classifyCalls int + findings []baseline.Finding } func (analyzer *countingBaselineSemanticAnalyzer) Segment( @@ -1402,6 +1411,10 @@ func (analyzer *countingBaselineSemanticAnalyzer) Segment( snapshot baseline.RuleSegmentationSnapshot, ) (baseline.RuleSegmentationProposal, error) { analyzer.segmentCalls++ + analyzer.findings = append(analyzer.findings, baseline.Finding{ + Code: "baseline.semantic.segmentation-proposal.discarded", + Message: "Codex test segmentation proposal was discarded: invalid range", + }) return baseline.ManualRuleSegmentationProposal(snapshot) } @@ -1410,6 +1423,10 @@ func (analyzer *countingBaselineSemanticAnalyzer) Classify( snapshot baseline.AnalysisSnapshot, ) (baseline.ClassificationProposal, error) { analyzer.classifyCalls++ + analyzer.findings = append(analyzer.findings, baseline.Finding{ + Code: "baseline.semantic.classification-proposal.discarded", + Message: "Codex test classification proposal was discarded: invalid destination", + }) proposal, err := baseline.ManualClassificationProposal(snapshot) if err != nil { return baseline.ClassificationProposal{}, err @@ -1433,6 +1450,12 @@ func (analyzer *countingBaselineSemanticAnalyzer) Classify( ) } +func (analyzer *countingBaselineSemanticAnalyzer) TakeFindings() []baseline.Finding { + findings := append([]baseline.Finding(nil), analyzer.findings...) + analyzer.findings = nil + return findings +} + func humanBaselineAdoptionAnswers(final string) string { answers := []string{ "1", diff --git a/internal/cli/releaseplan_documentation_contract_test.go b/internal/cli/releaseplan_documentation_contract_test.go index 2088aaaa..33268eea 100644 --- a/internal/cli/releaseplan_documentation_contract_test.go +++ b/internal/cli/releaseplan_documentation_contract_test.go @@ -89,6 +89,14 @@ func TestReleasePlanDocumentationContract(t *testing.T) { { name: "root Agent pointer", path: filepath.Join(repoRoot, "AGENTS.md"), + snippets: []string{ + "Repository-owned rules", + "docs/agents/specific-repository.md", + }, + }, + { + name: "repository-specific rules", + path: filepath.Join(repoRoot, "docs", "agents", "specific-repository.md"), snippets: []string{ "HARD RULE — release planning", "roundfix release plan", diff --git a/skills-lock.json b/skills-lock.json index 82c7709e..77e0ee79 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -167,6 +167,13 @@ "sourceType": "github", "skillPath": "skills/03-engineering-design/tui-design/SKILL.md", "computedHash": "92b969f3473842ee731e0c7a5c571e3b89e5dac7da81be5e635e1715b0084e6e" + }, + "exa-web-search": { + "source": "marcioaltoe/skills", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", + "sourceType": "github", + "skillPath": "skills/01-discovery/exa-web-search/SKILL.md", + "computedHash": "16a95a38b232f4746ff8785677f48615b5464fa36786a3fc2d23e7e2d1129d72" } } } diff --git a/skills/baseline_skill_contract_test.go b/skills/baseline_skill_contract_test.go index ea65556c..ac48ecc3 100644 --- a/skills/baseline_skill_contract_test.go +++ b/skills/baseline_skill_contract_test.go @@ -337,7 +337,7 @@ func TestAuthorialSkillSync(t *testing.T) { if err := json.Unmarshal(lockBytes, &lock); err != nil { t.Fatalf("decode skills lock: %v", err) } - const wantUpstreamDigest = "df289f03c4555e310822426f8521254b13f9befb87263d739c9739680f399814" + const wantUpstreamDigest = "4fb4367c8a086034727b70b23795712be9b436074f4ba6b9bc6ae5da352d21a7" if got := upstreamManagedSkillDigest(t, repoRoot, lock.Skills); got != wantUpstreamDigest { t.Fatalf("upstream-managed skill tree digest = %q, want %q", got, wantUpstreamDigest) } @@ -598,7 +598,7 @@ func TestAuthoringConstraintOwnership(t *testing.T) { }) } - const wantUpstreamDigest = "df289f03c4555e310822426f8521254b13f9befb87263d739c9739680f399814" + const wantUpstreamDigest = "4fb4367c8a086034727b70b23795712be9b436074f4ba6b9bc6ae5da352d21a7" if got := upstreamManagedSkillDigest(t, repoRoot, lock.Skills); got != wantUpstreamDigest { t.Fatalf("upstream-managed skill tree digest = %q, want %q", got, wantUpstreamDigest) } @@ -637,7 +637,7 @@ func TestUpstreamADRFormatUnchanged(t *testing.T) { _, _ = upstreamDigest.Write([]byte(name)) _, _ = upstreamDigest.Write([]byte(folderDigest)) } - const wantUpstreamDigest = "df289f03c4555e310822426f8521254b13f9befb87263d739c9739680f399814" + const wantUpstreamDigest = "4fb4367c8a086034727b70b23795712be9b436074f4ba6b9bc6ae5da352d21a7" if got := hex.EncodeToString(upstreamDigest.Sum(nil)); got != wantUpstreamDigest { t.Fatalf("upstream-managed skill tree digest = %q, want %q", got, wantUpstreamDigest) } diff --git a/skills/recommended.txt b/skills/recommended.txt index d8fd49cb..cdb7b198 100644 --- a/skills/recommended.txt +++ b/skills/recommended.txt @@ -5,6 +5,7 @@ coding-guidelines context7 conventional-commits domain-modeling +exa-web-search github-pr-workflow golang-cli golang-concurrency