@ericrisco/rsc 1.1.6 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,220 @@
1
+ const REVIEW_TOOLS = Object.freeze(['read', 'search']);
2
+ const RESOLVER_TOOLS = Object.freeze(['read', 'search', 'edit', 'shell']);
3
+
4
+ const reviewerBody = ({ label, scope, probes, escalation = '' }) => `You are the ${label} reviewer. Review the requested diff or pull request; do not edit files.
5
+
6
+ Boundary: ${scope}
7
+
8
+ Work from changed lines outward just far enough to verify callers, imports, guards, and tests. Record an **attack list** even when every attack is clean. For each proposed finding, answer before reporting:
9
+ 1. What exact changed line supports it?
10
+ 2. What concrete input and state produce what wrong result?
11
+ 3. Which caller, import, and relevant test did you inspect?
12
+ 4. Why do existing guards not reduce the severity?
13
+
14
+ HIGH and CRITICAL findings require the exact line and the full failure path. If either is missing, lower the severity or omit the finding. Prefer a clean verdict with a useful attack list over speculative volume.
15
+
16
+ Stack attacks:
17
+ ${probes.map((probe) => `- ${probe}`).join('\n')}
18
+ ${escalation ? `\n${escalation}\n` : ''}
19
+ Return: scope, attack list, findings ordered by severity, and verdict. A finding names file:line, failure mode, evidence, and the smallest test that would expose it.`;
20
+
21
+ const resolverBody = ({ label, scope, signals, attempts = 3 }) => `You are the ${label} build resolver. Diagnose and repair one failing build with the smallest stack-correct change.
22
+
23
+ Boundary: ${scope}
24
+
25
+ 1. Reproduce the named failure and preserve its exact command and first causal diagnostic.
26
+ 2. Trace that diagnostic to configuration, dependency, generated code, compiler, linker, or runtime setup.
27
+ 3. Change only what is needed for that cause; do not refactor, upgrade broadly, or clean unrelated warnings.
28
+ 4. Re-run the narrow failing command, then the nearest relevant test/build gate.
29
+ 5. Stop after ${attempts} unsuccessful repair attempts or as soon as the next step requires a refactor or product choice.
30
+
31
+ Signals to distinguish:
32
+ ${signals.map((signal) => `- ${signal}`).join('\n')}
33
+
34
+ Return the root cause, commands run, files changed, proof of the result, and any remaining blocker.`;
35
+
36
+ const reviewer = ({ name, label, skills, not, probes, should, shouldNot, tier = 'balanced', escalation }) => ({
37
+ name,
38
+ role: 'reviewer',
39
+ tier,
40
+ tools: [...REVIEW_TOOLS],
41
+ skills,
42
+ desc: `${label} reviewer: inspects changed code for stack-specific failure modes and reports evidence without editing. NOT ${not}.`,
43
+ routing: { should, shouldNot },
44
+ body: reviewerBody({ label, scope: `Use for ${skills.join(' or ')} code. NOT ${not}.`, probes, escalation }),
45
+ });
46
+
47
+ const resolver = ({ name, label, skills, not, signals, should, shouldNot, tier = 'balanced' }) => ({
48
+ name,
49
+ role: 'build-resolver',
50
+ tier,
51
+ tools: [...RESOLVER_TOOLS],
52
+ skills,
53
+ desc: `${label} build resolver: reproduces a failing build and applies the minimum verified repair. NOT ${not}.`,
54
+ routing: { should, shouldNot },
55
+ body: resolverBody({ label, scope: `Use for ${skills.join(' or ')} build failures. NOT ${not}.`, signals }),
56
+ });
57
+
58
+ const REVIEWERS = [
59
+ reviewer({
60
+ name: 'django-reviewer', label: 'Django', skills: ['django'], not: 'general Python review or build repair',
61
+ probes: ['QuerySet evaluation, N+1 reads, and accidental full-table work.', 'Transaction boundaries, row locking, retries, and side effects inside atomic blocks.', 'Migration reversibility, historical models, nullable transitions, and deploy ordering.', 'Authorization at object/query boundaries rather than only at the view.'],
62
+ should: ['Review a Django ORM diff for query and transaction defects.'], shouldNot: ['Fix a failing Django migration command.'],
63
+ }),
64
+ reviewer({
65
+ name: 'java-reviewer', label: 'Java and Spring', skills: ['java', 'spring-boot'], not: 'Android/Kotlin review or generic build repair',
66
+ probes: ['Nullability and Optional contracts across public boundaries.', 'Equality, hashing, mutability, and collection aliasing.', 'Spring proxy boundaries: transactions, self-invocation, scopes, and configuration binding.', 'Thread ownership, blocking work, interruption, and resource closure.'],
67
+ should: ['Review a Spring transaction change.'], shouldNot: ['Repair a Gradle dependency resolution failure.'],
68
+ }),
69
+ reviewer({
70
+ name: 'kotlin-reviewer', label: 'Kotlin Android', skills: ['kotlin-android'], not: 'Java server review or Gradle repair',
71
+ probes: ['Coroutine scope ownership, cancellation, dispatcher choice, and exception propagation.', 'Lifecycle collection, recomposition stability, and state restoration.', 'Nullable/platform types and sealed-state exhaustiveness.', 'Context/activity retention and Android resource lifetime.'],
72
+ should: ['Review a coroutine-driven Android screen.'], shouldNot: ['Resolve an Android Gradle Plugin mismatch.'],
73
+ }),
74
+ reviewer({
75
+ name: 'flutter-reviewer', label: 'Flutter', skills: ['flutter'], not: 'native Android review or Flutter build repair',
76
+ probes: ['Widget identity, keys, rebuild scope, and stale BuildContext use.', 'Controller, subscription, isolate, and animation disposal.', 'Async mounted checks and state changes after teardown.', 'Platform-channel contracts and divergent iOS/Android behavior.'],
77
+ should: ['Review a Flutter widget state diff.'], shouldNot: ['Fix a CocoaPods build failure.'],
78
+ }),
79
+ reviewer({
80
+ name: 'react-reviewer', label: 'React and Next.js', skills: ['react', 'nextjs'], not: 'Vue review or frontend build repair',
81
+ probes: ['Render purity, hook dependency identity, stale closures, and effect cleanup.', 'Server/client boundaries, serialization, caching, and request-scoped data.', 'Concurrent rendering races, transitions, hydration, and optimistic rollback.', 'Keys, controlled state, accessibility semantics, and error/loading states.'],
82
+ should: ['Review a Next.js server/client boundary.'], shouldNot: ['Repair a webpack or Turbopack build.'],
83
+ }),
84
+ reviewer({
85
+ name: 'rust-reviewer', label: 'Rust', skills: ['rust'], not: 'C++ review or Cargo build repair',
86
+ probes: ['Ownership workarounds that hide lifetime or aliasing defects.', 'Unsafe blocks: stated invariant, pointer provenance, layout, and synchronization.', 'Error propagation, panic reachability, and poisoned/shared state.', 'Async cancellation, Send/Sync assumptions, locks across await, and task lifetime.'],
87
+ should: ['Review an unsafe Rust boundary.'], shouldNot: ['Fix a Cargo feature conflict.'],
88
+ }),
89
+ reviewer({
90
+ name: 'swift-reviewer', label: 'Swift iOS', skills: ['swift-ios'], not: 'Kotlin review or Xcode build repair',
91
+ probes: ['Actor isolation, Sendable claims, cancellation, and main-thread ownership.', 'Reference cycles, escaping closures, and resource lifetime.', 'Value/reference semantics, copy-on-write, and mutation through shared state.', 'View identity, navigation state, persistence, and platform availability.'],
92
+ should: ['Review Swift concurrency changes in an iOS app.'], shouldNot: ['Resolve a signing or linker failure.'],
93
+ }),
94
+ reviewer({
95
+ name: 'go-reviewer', label: 'Go', skills: ['go'], not: 'generic service review or Go build repair',
96
+ probes: ['Goroutine lifetime, cancellation ownership, channel closure, and races.', 'Error wrapping and identity where callers use errors.Is or errors.As.', 'Interface nil values, slice/map aliasing, loop capture, and deferred cleanup lifetime.', 'API and doc-comment contracts, zero values, receiver choice, and lock copying.'],
97
+ should: ['Review Go concurrency and error handling.'], shouldNot: ['Repair a missing Go module or compile error.'],
98
+ }),
99
+ reviewer({
100
+ name: 'cpp-reviewer', label: 'C++', skills: ['cpp'], not: 'Rust review or compiler/linker repair',
101
+ probes: ['Ownership expressed through RAII and any raw pointer whose lifetime is unstated.', 'Dangling views/references, moved-from values, invalidation, and destructor behavior.', 'Bounds, narrowing, signedness, undefined behavior, and exception guarantees.', 'Synchronization, data races, virtual destruction, and unsafe casts.'],
102
+ should: ['Review a C++ ownership or lifetime diff.'], shouldNot: ['Resolve a CMake or linker failure.'],
103
+ }),
104
+ reviewer({
105
+ name: 'vue-reviewer', label: 'Vue and Nuxt', skills: ['vue-nuxt'], not: 'React review or Vue build repair',
106
+ probes: ['Reactive identity, destructuring, watcher cleanup, and computed side effects.', 'SSR/client divergence, hydration, route middleware, and request-scoped state.', 'Composable lifetime, provide/inject contracts, and error/loading states.', 'Template keys, accessibility semantics, and unsafe rendered content.'],
107
+ should: ['Review a Nuxt SSR and hydration diff.'], shouldNot: ['Fix a Vite build.'],
108
+ }),
109
+ reviewer({
110
+ name: 'csharp-reviewer', label: 'C# and .NET', skills: ['csharp-dotnet'], not: 'Java review or dotnet build repair',
111
+ probes: ['Async cancellation, sync-over-async, disposal, and scoped service lifetime.', 'Nullable annotations versus runtime validation and serializer behavior.', 'LINQ query translation, repeated enumeration, tracking, and transaction scope.', 'Records/equality, mutable shared state, authorization, and configuration binding.'],
112
+ should: ['Review an ASP.NET or EF Core change.'], shouldNot: ['Repair a NuGet restore failure.'],
113
+ }),
114
+ reviewer({
115
+ name: 'php-reviewer', label: 'PHP and Laravel', skills: ['laravel', 'php'], not: 'Django review or Composer/build repair',
116
+ probes: ['Request validation, mass assignment, policy scope, and tenant isolation.', 'Eloquent loading, query count, transactions, and queued work after commit.', 'PHP type coercion, nullable boundaries, array shape assumptions, and error conversion.', 'Migration deploy ordering, cache/config behavior, and serialization.'],
117
+ should: ['Review Laravel authorization and ORM code.'], shouldNot: ['Resolve a Composer dependency error.'],
118
+ }),
119
+ reviewer({
120
+ name: 'typescript-reviewer', label: 'TypeScript', skills: ['typescript'], not: 'framework-specific UI review or TypeScript build repair',
121
+ probes: ['Runtime validation at boundaries where static types disappear.', 'Unsound assertions, any/unknown narrowing, variance, and optional-property semantics.', 'Promise ownership, rejection paths, cancellation, and event-listener cleanup.', 'Module/export contracts and type/value divergence across build targets.'],
122
+ should: ['Review a TypeScript library API diff.'], shouldNot: ['Fix tsconfig or bundler errors.'],
123
+ }),
124
+ reviewer({
125
+ name: 'fastapi-reviewer', label: 'FastAPI', skills: ['fastapi'], not: 'generic Python review or environment repair',
126
+ probes: ['Dependency lifetime and cleanup, override leakage, and request-scoped state.', 'Async blocking, cancellation, background task ownership, and exception mapping.', 'Pydantic input/output boundaries, partial updates, and response data exposure.', 'Authorization per resource, transaction completion, and OpenAPI contract drift.'],
127
+ should: ['Review a FastAPI endpoint and dependency change.'], shouldNot: ['Repair a Python package installation.'],
128
+ }),
129
+ reviewer({
130
+ name: 'python-reviewer', label: 'Python', skills: ['python'], not: 'FastAPI/Django-specific review or environment repair',
131
+ probes: ['Async tasks that are never awaited, cancellation loss, and TaskGroup failure propagation.', 'Mutable defaults, late binding, descriptor/protocol behavior, and truthiness edge cases.', 'Context-manager and iterator cleanup, exception chaining, and generator finalization.', 'Shared mutable state, import-time effects, typing/runtime gaps, and process/thread boundaries.'],
132
+ should: ['Review Python language and asyncio correctness.'], shouldNot: ['Review Django ORM behavior.'],
133
+ }),
134
+ reviewer({
135
+ name: 'postgres-reviewer', label: 'PostgreSQL', skills: ['postgresdb'], not: 'application ORM review or migration execution',
136
+ probes: ['Lock strength/order, long transactions, deadlocks, and concurrent DDL behavior.', 'Index/operator compatibility, selectivity, null semantics, and query-plan regressions.', 'RLS policy coverage, SECURITY DEFINER/search_path, grants, and tenant isolation.', 'Migration reversibility, constraint validation, backfill batching, and replication effects.'],
137
+ should: ['Review a PostgreSQL migration and policy diff.'], shouldNot: ['Run or repair application migrations.'],
138
+ }),
139
+ reviewer({
140
+ name: 'rag-reviewer', label: 'RAG', skills: ['rag'], not: 'general ML training review or retrieval implementation',
141
+ probes: ['Evaluation set leakage, temporal leakage, and mismatch between offline and live queries.', 'Chunk/metadata identity, permission filtering before retrieval, and deletion propagation.', 'Recall/rerank trade-offs, empty retrieval, citation grounding, and abstention.', 'Embedding/model/index version compatibility and rollback behavior.'],
142
+ should: ['Review a retrieval pipeline for leakage and grounding.'], shouldNot: ['Tune a neural training loop.'],
143
+ }),
144
+ reviewer({
145
+ name: 'mle-reviewer', label: 'production ML', skills: ['machine-learning'], not: 'notebook style review or model training implementation', tier: 'heavy',
146
+ probes: ['Temporal/entity leakage in splits and features available only after prediction time.', 'Training-serving schema skew, data contracts, null/category drift, and feature ownership.', 'Promotion gates that fail open, canary comparability, monitoring delay, and rollback without retraining.', 'Reproducible artifacts: data/code/model lineage, seeds, environment, and deterministic evaluation.'],
147
+ should: ['Review an ML promotion and rollback pipeline.'], shouldNot: ['Implement or tune the model.'],
148
+ }),
149
+ ];
150
+
151
+ const RESOLVERS = [
152
+ resolver({ name: 'django-build-resolver', label: 'Django', skills: ['django'], not: 'ORM performance review or feature refactoring', signals: ['Migration graph conflicts versus unapplied migrations.', 'Settings/import errors versus database connectivity.', 'Static collection/template failures versus application tests.'], should: ['Fix a failing Django migrate/check command.'], shouldNot: ['Review an ORM diff for N+1 queries.'] }),
153
+ resolver({ name: 'java-build-resolver', label: 'Java and Spring', skills: ['java', 'spring-boot'], not: 'code review or broad dependency upgrades', signals: ['Compiler diagnostics versus annotation/generated-source failures.', 'Maven/Gradle resolution versus toolchain/JDK mismatch.', 'Spring context/bootstrap failures versus test assertion failures.'], should: ['Fix a Gradle/Maven/Spring build failure.'], shouldNot: ['Review transaction correctness.'] }),
154
+ resolver({ name: 'kotlin-build-resolver', label: 'Kotlin Android', skills: ['kotlin-android'], not: 'UI/coroutine review or project modernization', signals: ['Kotlin/AGP/Gradle/JDK compatibility.', 'KSP/KAPT/generated source failures.', 'Manifest/resource merge versus compiler diagnostics.'], should: ['Repair an Android Gradle build.'], shouldNot: ['Review Compose state.'] }),
155
+ resolver({ name: 'flutter-build-resolver', label: 'Flutter', skills: ['flutter'], not: 'widget review or package modernization', signals: ['Dart analyzer/compile errors versus generated-code drift.', 'CocoaPods/Xcode signing versus Android Gradle failures.', 'Plugin platform constraints versus app code failures.'], should: ['Repair a Flutter build failure.'], shouldNot: ['Review widget lifecycle.'] }),
156
+ resolver({ name: 'react-build-resolver', label: 'React and Next.js', skills: ['react', 'nextjs'], not: 'UI review or broad package upgrades', signals: ['Type/lint failure versus bundler/module resolution.', 'Server/client boundary or static rendering diagnostics.', 'Environment/config presence versus application logic.'], should: ['Repair a Next.js or React production build.'], shouldNot: ['Review hydration correctness.'] }),
157
+ resolver({ name: 'rust-build-resolver', label: 'Rust', skills: ['rust'], not: 'unsafe-code review or dependency modernization', signals: ['Borrow/type diagnostics versus feature unification.', 'Build-script/native library/linker failures.', 'Toolchain/edition mismatch versus source error.'], should: ['Repair a Cargo build failure.'], shouldNot: ['Review unsafe invariants.'] }),
158
+ resolver({ name: 'swift-build-resolver', label: 'Swift iOS', skills: ['swift-ios'], not: 'concurrency review or signing-policy changes', signals: ['Swift compiler/generic diagnostics versus package resolution.', 'Linker/module map/generated interface failure.', 'Simulator/device availability versus signing/provisioning.'], should: ['Repair an Xcode or SwiftPM build.'], shouldNot: ['Review actor isolation.'] }),
159
+ resolver({ name: 'go-build-resolver', label: 'Go', skills: ['go'], not: 'concurrency review or module-wide upgrades', signals: ['Compiler/type error versus generated code drift.', 'Module/version/sum failure versus private module credentials.', 'CGO/toolchain/linker failure versus test failure.'], should: ['Repair a go build failure.'], shouldNot: ['Review goroutine lifetime.'] }),
160
+ resolver({ name: 'cpp-build-resolver', label: 'C++', skills: ['cpp'], not: 'ownership review or build-system modernization', signals: ['Compiler diagnostic versus template instantiation cause.', 'CMake/configuration/generator failure versus source failure.', 'Undefined reference/ABI/library order versus runtime test failure.'], should: ['Repair a CMake/compiler/linker failure.'], shouldNot: ['Review C++ lifetime safety.'] }),
161
+ resolver({ name: 'pytorch-build-resolver', label: 'PyTorch', skills: ['machine-learning'], not: 'model-quality review or training redesign', signals: ['Python package/import mismatch versus compiled extension ABI.', 'CUDA/driver/toolkit/architecture incompatibility.', 'Distributed launcher/environment failure versus model code failure.'], should: ['Repair a PyTorch environment or extension build.'], shouldNot: ['Review data leakage or model promotion.'] }),
162
+ ];
163
+
164
+ const SPEC_MINER = {
165
+ name: 'spec-miner',
166
+ role: 'spec-miner',
167
+ tier: 'heavy',
168
+ tools: ['read', 'search', 'edit'],
169
+ skills: [],
170
+ desc: 'Brownfield spec miner: extracts an anchored SDD draft from existing code under a bounded read budget. NOT feature planning or overwriting an approved spec.',
171
+ routing: {
172
+ should: ['Extract an initial SDD spec from an existing repository with no specs.'],
173
+ shouldNot: ['Design a new feature or amend an approved spec in place.'],
174
+ },
175
+ body: `You are the brownfield spec miner. Produce an **extracted draft**, never a claim that undocumented intent is approved.
176
+
177
+ Read at most 40 files or 20,000 lines. Start from entry points, public interfaces, tests, schemas, migrations, configuration, and user-facing documentation. For every inferred invariant, cite a stable repository anchor (symbol plus path; line only as a convenience). Separate observed behavior, inferred requirement, contradiction, and unknown.
178
+
179
+ Write a new SDD-shaped draft with problem, users, observable behavior, invariants, non-goals, acceptance criteria, source anchors, and open points. Mark status extracted-draft. If the intended slug already has an approved spec, do not edit it: propose a new slug or a revision artifact. Do not plan implementation or invent product intent absent from evidence.
180
+
181
+ Return the read budget used, artifacts inspected, confidence per requirement, and the draft path.`,
182
+ };
183
+
184
+ const STACK_AGENTS = Object.freeze([...REVIEWERS, ...RESOLVERS, SPEC_MINER]);
185
+
186
+ export const STACK_AGENT_BY_SKILL = Object.freeze(Object.fromEntries(
187
+ ['django', 'java', 'spring-boot', 'kotlin-android', 'flutter', 'react', 'nextjs', 'rust', 'swift-ios', 'go', 'cpp', 'vue-nuxt', 'csharp-dotnet', 'laravel', 'php', 'typescript', 'fastapi', 'python', 'postgresdb', 'rag', 'machine-learning']
188
+ .map((skill) => [skill, STACK_AGENTS.filter((agent) => agent.skills.includes(skill)).map((agent) => agent.name)]),
189
+ ));
190
+
191
+ export const stackAgents = () => STACK_AGENTS.map((agent) => ({ ...agent, tools: [...agent.tools], skills: [...agent.skills], routing: { should: [...agent.routing.should], shouldNot: [...agent.routing.shouldNot] } }));
192
+ export const stackAgentNames = () => STACK_AGENTS.map((agent) => agent.name);
193
+ export const stackAgentByName = (name) => STACK_AGENTS.find((agent) => agent.name === name);
194
+
195
+ export function resolveStackAgentNames(skillIds = [], explicitAgentIds = []) {
196
+ const names = new Set();
197
+ for (const skill of skillIds) for (const name of STACK_AGENT_BY_SKILL[skill] || []) names.add(name);
198
+ for (const name of explicitAgentIds) if (stackAgentByName(name)) names.add(name);
199
+ return [...names].sort();
200
+ }
201
+
202
+ export function validateAgentCatalog(definitions = STACK_AGENTS) {
203
+ const errors = [];
204
+ const seen = new Set();
205
+ const allowedRoles = new Set(['reviewer', 'build-resolver', 'spec-miner']);
206
+ for (const agent of definitions) {
207
+ const prefix = agent?.name || '<unnamed>';
208
+ if (!agent?.name || seen.has(agent.name)) errors.push(`${prefix}: unique name required`);
209
+ seen.add(agent?.name);
210
+ if (!agent?.desc?.includes('NOT ')) errors.push(`${prefix}: description requires a NOT boundary`);
211
+ if (!allowedRoles.has(agent?.role)) errors.push(`${prefix}: invalid role`);
212
+ if (!['balanced', 'heavy'].includes(agent?.tier)) errors.push(`${prefix}: tier must be balanced or heavy`);
213
+ if (!agent?.routing?.should?.length || !agent?.routing?.shouldNot?.length) errors.push(`${prefix}: routing cases required`);
214
+ if (Buffer.byteLength(agent?.body || '', 'utf8') > 6500) errors.push(`${prefix}: body exceeds 6500 bytes`);
215
+ if (/everything[- ]claude|affaan|worldflow|\.claude\/plugins\/ecc/iu.test(`${agent?.desc || ''}\n${agent?.body || ''}`)) errors.push(`${prefix}: foreign project vocabulary`);
216
+ if (agent?.role === 'reviewer' && JSON.stringify(agent.tools) !== JSON.stringify(REVIEW_TOOLS)) errors.push(`${prefix}: reviewer tools must be read-only`);
217
+ if (agent?.role === 'build-resolver' && JSON.stringify(agent.tools) !== JSON.stringify(RESOLVER_TOOLS)) errors.push(`${prefix}: resolver tools must be bounded`);
218
+ }
219
+ return errors;
220
+ }
@@ -0,0 +1,48 @@
1
+ [
2
+ {
3
+ "agent": "go-reviewer",
4
+ "researchedAt": "2026-09-02",
5
+ "sources": [
6
+ {
7
+ "url": "https://go.dev/wiki/CodeReviewComments",
8
+ "coverage": ["error identity and wrapping", "goroutine lifetime", "interface values", "receiver and API review"]
9
+ },
10
+ {
11
+ "url": "https://go.dev/doc/comment",
12
+ "coverage": ["public API comments", "package and symbol contracts"]
13
+ },
14
+ {
15
+ "url": "https://go.dev/doc/contribute",
16
+ "coverage": ["review evidence and change scope"]
17
+ }
18
+ ]
19
+ },
20
+ {
21
+ "agent": "cpp-reviewer",
22
+ "researchedAt": "2026-09-02",
23
+ "sources": [
24
+ {
25
+ "url": "https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines.html",
26
+ "coverage": ["RAII and ownership", "lifetime and bounds safety", "concurrency", "exception guarantees"]
27
+ }
28
+ ]
29
+ },
30
+ {
31
+ "agent": "python-reviewer",
32
+ "researchedAt": "2026-09-02",
33
+ "sources": [
34
+ {
35
+ "url": "https://docs.python.org/3.14/library/asyncio-dev.html",
36
+ "coverage": ["un-awaited coroutines", "blocking async code", "thread boundaries"]
37
+ },
38
+ {
39
+ "url": "https://docs.python.org/3.14/library/asyncio-task.html",
40
+ "coverage": ["TaskGroup failure propagation", "cancellation", "task lifetime"]
41
+ },
42
+ {
43
+ "url": "https://docs.python.org/3.14/reference/datamodel.html",
44
+ "coverage": ["descriptors and protocols", "object and iterator lifetime"]
45
+ }
46
+ ]
47
+ }
48
+ ]
package/targets/agents.js CHANGED
@@ -10,6 +10,10 @@
10
10
  // written by `init` at onboarding and read here so re-syncs honor it.
11
11
  import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
12
12
  import { join, dirname } from 'node:path';
13
+ import {
14
+ stackAgents, stackAgentNames, stackAgentByName,
15
+ resolveStackAgentNames, validateAgentCatalog,
16
+ } from './agent-catalog.js';
13
17
 
14
18
  // Concrete model per provider per tier. June 2026 defaults — EDIT to your account's
15
19
  // models; the TIER is the contract, the id is yours to change. `light` is deliberately
@@ -42,13 +46,15 @@ export function targetHasAgents(target) { return Boolean(AGENT_TARGETS[target]);
42
46
  // (decided in clarify 2026-08-18: a lens passed as a parameter reintroduces the
43
47
  // does-anyone-remember dependency this whole spec exists to remove) — but P5 says length is a cost,
44
48
  // so the contract they share is composed, not written three times.
45
- const REFUTER_CONTRACT = `Your mandate is to **refute readiness**, not confirm it. A reviewer looking for confirmation finds confirmation; the asymmetry is the point.
46
-
47
- **You get exactly four inputs, and nothing else:**
49
+ export const REFUTER_FOUR_INPUTS = `**You get exactly four inputs, and nothing else:**
48
50
  1. The task contract — the original request **plus every scope change a human explicitly approved since**. Without the approved changes, a legitimate scope revision reads as a spec gap and you will report a confident false positive.
49
51
  2. The approved spec.
50
52
  3. The exact source state (commit SHA, or a tree hash when git is absent). A verdict attaches to the state you saw, not to the project.
51
- 4. The entry point — the one command that reruns the checks.
53
+ 4. The entry point — the one command that reruns the checks.`;
54
+
55
+ const REFUTER_CONTRACT = `Your mandate is to **refute readiness**, not confirm it. A reviewer looking for confirmation finds confirmation; the asymmetry is the point.
56
+
57
+ ${REFUTER_FOUR_INPUTS}
52
58
 
53
59
  **You do NOT get** the builder's conversation, reasoning, defences, or draft verdict. If a claim needs the builder's justification to stand, it is not proven.
54
60
 
@@ -56,6 +62,16 @@ const REFUTER_CONTRACT = `Your mandate is to **refute readiness**, not confirm i
56
62
 
57
63
  **The attack list is the deliverable, not just the findings.** "Nothing found" without saying where you looked is indistinguishable from not having looked.
58
64
 
65
+ **Before reporting any finding, answer all four questions:**
66
+ 1. Can you cite the **exact changed line**?
67
+ 2. Can you state the **concrete input, state, and wrong result**? The concrete input and state must be explicit.
68
+ 3. Did you inspect the relevant **caller, import, and relevant test**?
69
+ 4. Can the severity survive the **existing guards** you verified?
70
+
71
+ If an answer is no, lower the severity or omit the finding. Every **HIGH or CRITICAL** needs the line and failure mode in the report. **Zero findings with an attack list is valid.**
72
+
73
+ **Common false positives to reject:** an equivalent mutant with no diverging input; a documented dummy value that never reaches a sink; a deliberate boundary already enforced by a caller; generated/vendor code outside the change; style preference presented as correctness; and a theoretical race with no shared state or overlapping lifetime.
74
+
59
75
  **A finding blocks only if it is caused by this change, is severe, and carries evidence** — a repro or a concrete failure scenario. A suspicion without one is a question, and questions do not block. You fix nothing: findings return through the normal loop, and a SPEC gap goes to the human, never to the builder to self-amend.`;
60
76
 
61
77
  const AGENTS = [
@@ -112,11 +128,18 @@ ${REFUTER_CONTRACT}
112
128
  },
113
129
  ];
114
130
 
131
+ export const BASE_AGENT_NAMES = Object.freeze(AGENTS.map((agent) => agent.name));
132
+ export { stackAgents, stackAgentNames, validateAgentCatalog };
133
+ export const allAgentNames = () => [...BASE_AGENT_NAMES, ...stackAgentNames()];
134
+ export function resolveAgentNames(skillIds = [], explicitAgentIds = []) {
135
+ return [...BASE_AGENT_NAMES, ...resolveStackAgentNames(skillIds, explicitAgentIds)];
136
+ }
137
+
115
138
  // Back-compat: the tier file and its reader are named for `developer` because that is what they
116
139
  // configure. The refuters run at the same tier — a deliberate, reversible default: nobody has measured
117
140
  // whether a heavier model finds more here, and silently tripling tier-2 cost on an unmeasured hunch is
118
141
  // the wrong way to find out.
119
- const byName = (name) => AGENTS.find((a) => a.name === name);
142
+ const byName = (name) => AGENTS.find((a) => a.name === name) || stackAgentByName(name);
120
143
  export const agentNames = () => AGENTS.map((a) => a.name);
121
144
 
122
145
  // `.rsc/developer.json` — the chosen tier (balanced default; never light). `init` writes
@@ -137,14 +160,16 @@ export function writeDeveloperTier(cwd, tier) {
137
160
  function renderMd(spec, model, agent) {
138
161
  const fm = ['---', `name: ${agent.name}`, `description: "${agent.desc}"`, `model: ${model}`];
139
162
  if (spec.mode) fm.push(`mode: ${spec.mode}`);
163
+ if (agent.tools) fm.push(`tools: [${agent.tools.join(', ')}]`);
140
164
  fm.push('---', '');
141
165
  return `${fm.join('\n')}${agent.body}\n`;
142
166
  }
143
- const renderJson = (model, agent) => `${JSON.stringify({ name: agent.name, description: agent.desc, model, prompt: agent.body }, null, 2)}\n`;
167
+ const renderJson = (model, agent) => `${JSON.stringify({ name: agent.name, description: agent.desc, model, ...(agent.tools ? { tools: agent.tools } : {}), prompt: agent.body }, null, 2)}\n`;
144
168
  function renderToml(model, agent) {
145
169
  const esc = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
146
170
  // body as a TOML multiline LITERAL string ('''…''') — no escape processing.
147
- return `name = "${agent.name}"\ndescription = "${esc(agent.desc)}"\nmodel = "${model}"\ndeveloper_instructions = '''\n${agent.body}\n'''\n`;
171
+ const tools = agent.tools ? `tools = [${agent.tools.map((tool) => `"${tool}"`).join(', ')}]\n` : '';
172
+ return `name = "${agent.name}"\ndescription = "${esc(agent.desc)}"\nmodel = "${model}"\n${tools}developer_instructions = '''\n${agent.body}\n'''\n`;
148
173
  }
149
174
 
150
175
  export function agentPath(target, cwd, name = 'developer') {
@@ -152,12 +177,15 @@ export function agentPath(target, cwd, name = 'developer') {
152
177
  return spec ? join(cwd, ...spec.dir.split('/'), `${name}${spec.ext}`) : null;
153
178
  }
154
179
 
155
- export function writeAgents(target, cwd, tier = readDeveloperTier(cwd)) {
180
+ export function writeAgents(target, cwd, tier = readDeveloperTier(cwd), names = agentNames()) {
156
181
  const spec = AGENT_TARGETS[target];
157
182
  if (!spec) return [];
158
- const model = spec.model(tier);
159
183
  const written = [];
160
- for (const agent of AGENTS) {
184
+ for (const name of names) {
185
+ const agent = byName(name);
186
+ if (!agent) continue;
187
+ const effectiveTier = agent.tier === 'heavy' ? 'heavy' : tier;
188
+ const model = spec.model(effectiveTier);
161
189
  const content = spec.format === 'json' ? renderJson(model, agent)
162
190
  : spec.format === 'toml' ? renderToml(model, agent)
163
191
  : renderMd(spec, model, agent);
@@ -169,14 +197,34 @@ export function writeAgents(target, cwd, tier = readDeveloperTier(cwd)) {
169
197
  return written;
170
198
  }
171
199
 
200
+ export function reconcileAgents(target, cwd, tier, previousNames = [], desiredNames = []) {
201
+ if (!targetHasAgents(target)) return { written: [], removed: [], collisions: [], names: [] };
202
+ const previous = new Set(previousNames);
203
+ const desired = new Set(desiredNames);
204
+ const removed = removeAgents(target, cwd, [...previous].filter((name) => !desired.has(name)));
205
+ const written = [];
206
+ const collisions = [];
207
+ const names = [];
208
+ for (const name of desiredNames) {
209
+ const path = agentPath(target, cwd, name);
210
+ if (existsSync(path) && !previous.has(name)) {
211
+ collisions.push(path);
212
+ continue;
213
+ }
214
+ written.push(...writeAgents(target, cwd, tier, [name]));
215
+ if (existsSync(path)) names.push(name);
216
+ }
217
+ return { written, removed, collisions, names };
218
+ }
219
+
172
220
  /**
173
221
  * Remove only the agents this catalog ships. An uninstaller that takes an agent the user wrote by hand
174
222
  * is worse than one that leaves residue.
175
223
  */
176
- export function removeAgents(target, cwd) {
224
+ export function removeAgents(target, cwd, names = allAgentNames()) {
177
225
  const removed = [];
178
- for (const agent of AGENTS) {
179
- const path = agentPath(target, cwd, agent.name);
226
+ for (const name of names) {
227
+ const path = agentPath(target, cwd, name);
180
228
  if (path && existsSync(path)) { rmSync(path, { force: true }); removed.push(path); }
181
229
  }
182
230
  return removed;
package/targets/claude.js CHANGED
@@ -85,6 +85,9 @@ export function wireHook(paths) {
85
85
  // Shared by session-start + userprompt-gate. Hooks are materialized file by file, so a module
86
86
  // they import must be copied as their SIBLING — a subdirectory import would break once copied.
87
87
  copyFileSync(join(HERE, 'hook-once.mjs'), join(paths.projectRoot, '.rsc', 'hook-once.mjs'));
88
+ // Same reason: session-start imports the reaper for the landed-worktree sweep, so it travels as a
89
+ // sibling too. Missing it does not break startup — the sweep is wrapped — it just goes silent.
90
+ copyFileSync(join(HERE, 'worktree-reaper.mjs'), join(paths.projectRoot, '.rsc', 'worktree-reaper.mjs'));
88
91
  copyFileSync(join(HERE, 'session-start.mjs'), scriptDest);
89
92
 
90
93
  const suggestMd = `${paths.skillDir('suggest')}/SKILL.md`;