chore(template): clean-slate template snapshot from bb4a0c7
Curated, product-agnostic snapshot of the post-story-04 tree: demo content deleted, auth-only reference feature, web-next shell, all gates green. Product-specific docs, ADRs 027-029, PRDs/epics/archive, editor library traces, and product naming are curated out; generic template repairs (coverage provider devDeps, root test:coverage script, live lint fixes, root-only release-please) are kept. See TEMPLATE.md for provenance, curation list, and usage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
91
.claude/skills/evaluate-library/EXAMPLES/approved-example.md
Normal file
91
.claude/skills/evaluate-library/EXAMPLES/approved-example.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
package: clsx
|
||||
version: "^2.1.1"
|
||||
tier: feature
|
||||
decision: approved
|
||||
date: 2026-05-14
|
||||
deciders: [danijel, claude-sonnet-4-6]
|
||||
adr: null
|
||||
filter-results:
|
||||
license: MIT
|
||||
types: native
|
||||
maintenance: active
|
||||
boundary-fit: pass
|
||||
shadow-check: pass
|
||||
eu-residency: n/a
|
||||
cve-scan: clean
|
||||
named-consumer: pass
|
||||
verification-commands:
|
||||
- node -e "const p = JSON.parse(require('fs').readFileSync('./node_modules/clsx/package.json','utf8')); console.log(p.license)"
|
||||
- ls node_modules/clsx/dist/clsx.d.ts
|
||||
- npm view clsx time.modified
|
||||
- pnpm audit --audit-level=moderate
|
||||
accepted-cves: []
|
||||
---
|
||||
|
||||
## Filter: license
|
||||
|
||||
<!-- Result: MIT -->
|
||||
|
||||
`package.json` declares `"license": "MIT"`. MIT is on the allowlist. Pass.
|
||||
|
||||
## Filter: types
|
||||
|
||||
<!-- Result: native -->
|
||||
|
||||
`clsx` ships its own `.d.ts` declarations at `dist/clsx.d.ts`. No `@types/clsx` package needed.
|
||||
The TypeScript surface covers the full public API (`ClassValue`, overloads). Pass.
|
||||
|
||||
## Filter: maintenance
|
||||
|
||||
<!-- Result: active -->
|
||||
|
||||
Last npm release: `2.1.1` published 2024-02-06 (15 months ago at evaluation date — within the 18-month threshold). GitHub shows open PR/issue activity within the last 3 months. The library is small, intentionally stable, and actively maintained. Pass.
|
||||
|
||||
## Filter: boundary-fit
|
||||
|
||||
<!-- Result: pass -->
|
||||
|
||||
`clsx` is a pure string-concatenation utility. It imports nothing from Node.js or browser globals; it has zero transitive dependencies. Adding it to `packages/navigation` as a `feature`-tagged package introduces no boundary violations under ADR-006 or ADR-010. It does not import `@sentry/*`, `@opentelemetry/*`, or any core-reserved vendor SDK. Pass.
|
||||
|
||||
## Filter: shadow-check
|
||||
|
||||
<!-- Result: pass -->
|
||||
|
||||
The locked workspace must-haves are: `zod`, `inversify`, `payload`, `@trpc/server`, `superjson`, `reflect-metadata`. None of these perform CSS class-name composition. There is no existing utility in the workspace for this purpose. Pass.
|
||||
|
||||
## Filter: eu-residency
|
||||
|
||||
<!-- Result: n/a -->
|
||||
|
||||
`clsx` is a pure in-process string utility. It performs no network calls, transmits no user data, and has no SaaS endpoint. EU residency filter does not apply.
|
||||
|
||||
## Filter: cve-scan
|
||||
|
||||
<!-- Result: clean -->
|
||||
|
||||
`pnpm audit --audit-level=moderate` returns 0 vulnerabilities for `clsx@2.1.1` at evaluation time. No accepted advisories.
|
||||
|
||||
## Filter: named-consumer
|
||||
|
||||
<!-- Result: pass -->
|
||||
|
||||
Named consumer: `packages/navigation/src/ui/components/navigation-menu.tsx` — the `NavigationMenuLink` component must compute conditional class names for the active/inactive link state. Without `clsx` this is implemented as a ternary chain that becomes unreadable past three conditions. The component exists today; this is not a hypothetical future use case.
|
||||
|
||||
Secondary consumer: `packages/navigation/src/ui/components/mobile-nav.tsx` — open-state drawer overlay class computation. Both components are blocked on this adoption.
|
||||
|
||||
## Prompt: replaces
|
||||
|
||||
Replaces inline ternary chains like `` `base-class ${isActive ? 'active' : ''} ${isDisabled ? 'disabled' : ''}` ``. No library is being retired — this is a first-time adoption of a class-composition utility. No parallel adoption risk.
|
||||
|
||||
## Prompt: migration-cost-out
|
||||
|
||||
**Mechanical.** `clsx` is called only at the component leaf level. Removal means replacing `clsx(...)` calls with equivalent template-literal ternaries — a mechanical sed-style refactor bounded to the `packages/navigation/src/ui/` subtree. No data format dependencies, no vendor lock-in, no protocol coupling.
|
||||
|
||||
## Prompt: alternatives-considered
|
||||
|
||||
1. **`classnames`** — functional equivalent, MIT, widely used. Rejected in favour of `clsx` because `clsx` is the successor written by the same author with better TypeScript support and 2× faster benchmarks at comparable bundle size (330 B vs 440 B minzipped). `classnames` would also pass all eight filters; `clsx` is strictly preferable.
|
||||
|
||||
2. **Inline ternary chains (no library)** — the current approach. Adequate for one or two conditions; degrades rapidly past three. The `navigation-menu` component already has four conditional classes; this is the threshold where a utility library pays for itself. Rejected as the status quo.
|
||||
|
||||
3. **`tailwind-merge`** — superset of `clsx` that also de-duplicates conflicting Tailwind classes. Overkill for this use case (navigation components use a small, non-conflicting class set). Higher migration cost out (data-format dependency on Tailwind class semantics). Deferred.
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
package: trpc-to-openapi
|
||||
version: "^1.2.0"
|
||||
tier: core
|
||||
decision: rejected
|
||||
date: 2026-05-14
|
||||
deciders: [danijel, claude-sonnet-4-6]
|
||||
adr: null
|
||||
filter-results:
|
||||
license: MIT
|
||||
types: native
|
||||
maintenance: active
|
||||
boundary-fit: pass
|
||||
shadow-check: pass
|
||||
eu-residency: n/a
|
||||
cve-scan: clean
|
||||
named-consumer: fail
|
||||
verification-commands:
|
||||
- npm info trpc-to-openapi license
|
||||
- npm info trpc-to-openapi time.modified
|
||||
- pnpm audit --audit-level=moderate
|
||||
accepted-cves: []
|
||||
---
|
||||
|
||||
## Filter: license
|
||||
|
||||
<!-- Result: MIT -->
|
||||
|
||||
`package.json` declares `"license": "MIT"`. On the allowlist. Pass.
|
||||
|
||||
## Filter: types
|
||||
|
||||
<!-- Result: native -->
|
||||
|
||||
`trpc-to-openapi` ships TypeScript declarations (`.d.ts`) alongside the compiled output. Full API surface typed. Pass.
|
||||
|
||||
## Filter: maintenance
|
||||
|
||||
<!-- Result: active -->
|
||||
|
||||
Last npm release: `1.2.0` published within the past 12 months at evaluation date. GitHub shows active issue triage. Pass.
|
||||
|
||||
## Filter: boundary-fit
|
||||
|
||||
<!-- Result: pass -->
|
||||
|
||||
`trpc-to-openapi` would land in a `core`-tagged package alongside the tRPC router configuration. Core packages are permitted to hold tRPC-adjacent tooling. The library imports `@trpc/server` (already a workspace must-have) and standard `zod` types. No boundary violations under ADR-006 or ADR-010. Pass.
|
||||
|
||||
## Filter: shadow-check
|
||||
|
||||
<!-- Result: pass -->
|
||||
|
||||
No existing workspace library performs OpenAPI spec generation from tRPC routers. `trpc-to-openapi` does not duplicate any locked must-have. Pass.
|
||||
|
||||
## Filter: eu-residency
|
||||
|
||||
<!-- Result: n/a -->
|
||||
|
||||
`trpc-to-openapi` is a pure in-process code-generation utility. It produces an OpenAPI JSON spec at build time or request time; it transmits nothing to a vendor endpoint. EU residency filter does not apply.
|
||||
|
||||
## Filter: cve-scan
|
||||
|
||||
<!-- Result: clean -->
|
||||
|
||||
`pnpm audit --audit-level=moderate` returns 0 vulnerabilities at evaluation time. Pass.
|
||||
|
||||
## Filter: named-consumer
|
||||
|
||||
<!-- Result: fail -->
|
||||
|
||||
**No named consumer exists.**
|
||||
|
||||
The proposal arose during a 2026-05-14 grill session exploring whether to expose the tRPC router surface as a REST API for external consumers. The session established that **all current callers are TypeScript** and use `createCaller` directly — there are no HTTP REST clients calling the API, and no external consumers are blocked waiting for an OpenAPI spec.
|
||||
|
||||
The hypothetical consumers cited were:
|
||||
|
||||
- "External partners might want a REST API someday" — speculative; no partner is waiting.
|
||||
- "A mobile client might prefer REST over tRPC-HTTP" — hypothetical; no mobile client exists.
|
||||
- "OpenAPI docs improve DX for third-party integrations" — no third-party integration is in flight.
|
||||
|
||||
The grill-session question "who calls this code path today, or who is blocked waiting for it?" had the honest answer: nobody. The library would have shipped approximately 30 lines of `.meta({...})` annotations per router and a `superjson`-incompatible HTTP handler configuration in exchange for zero downstream consumers — pure carrying cost.
|
||||
|
||||
This trace exists as a permanent record per ADR-022 §4 so future agents do not re-evaluate `trpc-to-openapi` without first answering whether a named consumer now exists. If a concrete external integration is later planned, re-open this evaluation with the integration as the named consumer, re-run all eight filters, and write a new trace.
|
||||
|
||||
## Prompt: replaces
|
||||
|
||||
Nothing is being retired. The adoption would have been additive alongside the existing `createCaller` usage path.
|
||||
|
||||
## Prompt: migration-cost-out
|
||||
|
||||
**Hard.** Once `.meta({...})` annotations are added to tRPC procedures, they accumulate across routers over time. Removal requires stripping those annotations, deleting the OpenAPI spec generation step, and coordinating with any REST consumers that may have formed since adoption. The `superjson`-incompatible HTTP handler creates a parallel request path that would need to be decommissioned. Hard-rated because of the scattered annotation surface.
|
||||
|
||||
## Prompt: alternatives-considered
|
||||
|
||||
1. **`@anatine/zod-nestjs` + NestJS** — full REST framework alternative; overkill for a tRPC-native repo and would require replacing the tRPC layer entirely. Not a serious alternative for this use case.
|
||||
|
||||
2. **Custom OpenAPI spec, hand-authored** — maintain a `openapi.yaml` alongside the tRPC router. Zero runtime cost; no dependency; the spec is always exactly what consumers need. Viable if a named consumer materialises and the schema surface is stable. The correct path when named-consumer passes.
|
||||
|
||||
3. **No action (status quo)** — current approach: TypeScript callers use `createCaller`; no REST surface exposed. Correct given that no named consumer exists today. This is the chosen outcome.
|
||||
110
.claude/skills/evaluate-library/POLICY.md
Normal file
110
.claude/skills/evaluate-library/POLICY.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Library Evaluation Policy — Quick Reference
|
||||
|
||||
> Authoritative source: `docs/decisions/adr-022-library-evaluation-policy.md`
|
||||
> Authoritative runbook: `.claude/skills/evaluate-library/SKILL.md`
|
||||
|
||||
---
|
||||
|
||||
## Why this policy exists
|
||||
|
||||
The repo ships with a deliberately narrow runtime surface (six deps per feature package). That discipline is uncodified. Three signals exposed the gap: a near-miss adding `trpc-to-openapi` for hypothetical REST consumers; three ADRs recording library choices _after_ adoption; and no EU-residency gate before a library could silently transmit user data to a US-only SaaS endpoint. ADR-022 codifies the discipline and makes it agent-runnable.
|
||||
|
||||
---
|
||||
|
||||
## Tier trigger
|
||||
|
||||
The policy applies to **direct runtime dependencies** in feature- and core-tier packages. Devdeps and app-tier deps are exempt.
|
||||
|
||||
| Where the dep lands | Process required | Companion record |
|
||||
| -------------------------- | ------------------------- | ---------------- |
|
||||
| `apps/<x>` | Author's call — no policy | — |
|
||||
| `packages/<feature>` | Trace required | — |
|
||||
| `packages/core-*` | Trace required | ADR required |
|
||||
| New optional-core category | Trace required | ADR required |
|
||||
|
||||
The trigger maps onto the existing ESLint `boundaries` tag system (ADR-006, ADR-010) — no new mental model.
|
||||
|
||||
---
|
||||
|
||||
## Eight hard auto-reject filters
|
||||
|
||||
**Phase 1 — cheap (always run to completion)**
|
||||
|
||||
| # | Filter | Auto-reject condition |
|
||||
| --- | ---------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | **license** | Outside `MIT`, `Apache-2.0`, `BSD-*`, `ISC`, `MPL-2.0` |
|
||||
| 2 | **types** | No `.d.ts` and no `@types/<pkg>` |
|
||||
| 3 | **shadow-check** | Functional parallel to a locked must-have (`zod`, `inversify`, `payload`, `@trpc/server`, `superjson`, `reflect-metadata`) |
|
||||
| 4 | **boundary-fit** | Dep would violate ESLint boundary rules for the target tier (e.g., `@sentry/node` in a feature package — ADR-017 §4) |
|
||||
|
||||
**Phase 2 — expensive (short-circuit after first reject)**
|
||||
|
||||
| # | Filter | Auto-reject condition |
|
||||
| --- | ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| 5 | **maintenance** | Last release ≥ 18 months OR activity gap ≥ 12 months (`abandoned`) |
|
||||
| 6 | **cve-scan** | Open advisory at `moderate` severity or above (via `pnpm audit`) |
|
||||
| 7 | **eu-residency** | Library transmits user data/telemetry to a vendor endpoint with no EU data region available or not configured |
|
||||
| 8 | **named-consumer** | No concrete call site exists today and no feature is blocked waiting for it — hypothetical future use does not qualify |
|
||||
|
||||
A single failure in any filter → `decision: rejected`. Cheap filters always run; expensive filters stop at the first fail.
|
||||
|
||||
---
|
||||
|
||||
## Three discussion prompts
|
||||
|
||||
Not auto-reject filters — any answer is acceptable with justification. Required in every trace.
|
||||
|
||||
1. **replaces** — What existing approach does this replace? Parallel adoption of the same capability is a smell.
|
||||
2. **migration-cost-out** — Rate the removal cost 18 months from now: mechanical / hard / impossible.
|
||||
3. **alternatives-considered** — Two named alternatives minimum. For core-tier, also duplicated into the companion ADR.
|
||||
|
||||
---
|
||||
|
||||
## Trace artifact
|
||||
|
||||
Every decision — approved or rejected — produces a file at `docs/library-decisions/<YYYY-MM-DD>-<package-name>.md`.
|
||||
|
||||
**Required frontmatter fields:**
|
||||
|
||||
| Field | Values |
|
||||
| ------------------------------- | ------------------------------------------ |
|
||||
| `package` | npm package name |
|
||||
| `version` | semver range |
|
||||
| `tier` | `app` \| `feature` \| `core` |
|
||||
| `decision` | `approved` \| `rejected` |
|
||||
| `date` | `YYYY-MM-DD` |
|
||||
| `deciders` | list of authors (human and/or agent) |
|
||||
| `adr` | `adr-NNN` or `null` |
|
||||
| `filter-results.license` | SPDX id |
|
||||
| `filter-results.types` | `native` \| `@types/<x>` \| `none` |
|
||||
| `filter-results.maintenance` | `active` \| `dormant` \| `abandoned` |
|
||||
| `filter-results.boundary-fit` | `pass` \| `fail` |
|
||||
| `filter-results.shadow-check` | `pass` \| `fail` \| `"shadows <x>"` |
|
||||
| `filter-results.eu-residency` | `ok` \| `n/a` \| `self-hostable` \| `fail` |
|
||||
| `filter-results.cve-scan` | `clean` \| advisory ID \| `fail` |
|
||||
| `filter-results.named-consumer` | `pass` \| `fail` |
|
||||
| `verification-commands` | list of literal commands run |
|
||||
| `accepted-cves` | list of accepted advisory IDs (optional) |
|
||||
|
||||
Skipped expensive filters (short-circuited by an earlier reject) → write `skip` as the frontmatter value and note "Not evaluated" in the prose section.
|
||||
|
||||
The trace lands in **the same commit** as the `package.json` change. The pre-commit hook validates this for approved traces.
|
||||
|
||||
---
|
||||
|
||||
## Four-layer enforcement stack
|
||||
|
||||
| Layer | Latency | Catches |
|
||||
| ----------------------------------------------------------- | ---------- | ----------------------------------------------------------------- |
|
||||
| Claude `PreToolUse`/`PostToolUse` hook | inline | Agent skipping the skill before `pnpm add` or `package.json` edit |
|
||||
| `/evaluate-library` skill | seconds | The decision itself + writes the trace |
|
||||
| Git pre-commit hook (`scripts/library-decisions/check.mjs`) | pre-commit | Humans or agents bypassing the skill |
|
||||
| Sandcastle reviewer prompt | per-slice | Bypasses that slipped past pre-commit |
|
||||
|
||||
The Claude hook injects a `<system-reminder>` pointing to this skill. It is non-blocking — devdep additions and app-tier changes trigger the reminder but do not require a trace. The pre-commit hook is the deterministic gate.
|
||||
|
||||
---
|
||||
|
||||
## Composition with generators
|
||||
|
||||
`pnpm turbo gen core-package <name>` emits **pre-shipped traces** — one per direct runtime dep of the new core package — pre-marked `decision: approved` and citing the relevant ADR (ADR-015 for events, ADR-016 for realtime, ADR-018 for audit). No separate evaluation needed for scaffolded optional cores.
|
||||
308
.claude/skills/evaluate-library/SKILL.md
Normal file
308
.claude/skills/evaluate-library/SKILL.md
Normal file
@@ -0,0 +1,308 @@
|
||||
---
|
||||
name: evaluate-library
|
||||
description: Walk the 9-filter + 3-prompt library evaluation protocol for a named package, write the decision trace to docs/library-decisions/, and return pass/fail. Use when adding a runtime dependency to a feature or core package, or when the library-policy-nudge hook fires.
|
||||
---
|
||||
|
||||
<invocation>
|
||||
|
||||
```
|
||||
/evaluate-library <package-name> --tier <feature|core|app> --target <package-path>
|
||||
```
|
||||
|
||||
All three arguments are required. The `library-policy-nudge` hook emits this exact invocation. For `app`-tier packages, evaluation still runs but a trace is optional (author's call per ADR-022 §1).
|
||||
|
||||
</invocation>
|
||||
|
||||
<runbook>
|
||||
|
||||
## Overview
|
||||
|
||||
Walk nine hard auto-reject filters in **collect-cheap-skip-expensive** order, then answer three discussion prompts. Write the trace unconditionally at the end — including for rejections. A rejection trace is a permanent record that prevents future agents from re-litigating the same decision.
|
||||
|
||||
## Phase 1 — Cheap filters (always run to completion, even if one fails)
|
||||
|
||||
Run all four cheap filters regardless of their outcomes. Record each result before moving to Phase 2.
|
||||
|
||||
### Filter 1: license
|
||||
|
||||
Command: `node -e "const p = JSON.parse(require('fs').readFileSync('./node_modules/<pkg>/package.json','utf8')); console.log(p.license)"`
|
||||
|
||||
Allowlist: `MIT`, `Apache-2.0`, `BSD-2-Clause`, `BSD-3-Clause`, `ISC`, `MPL-2.0`.
|
||||
|
||||
Result values: the SPDX identifier (e.g. `MIT`) if allowed, or `<SPDX-id> (rejected)` if outside the allowlist. Anything outside the allowlist is an automatic reject but does not stop Phase 1.
|
||||
|
||||
### Filter 2: types
|
||||
|
||||
Check whether TypeScript types ship with the package or via `@types/<pkg>`:
|
||||
|
||||
```
|
||||
ls node_modules/<pkg>/index.d.ts 2>/dev/null && echo native || npm info @types/<pkg> version 2>/dev/null | head -1
|
||||
```
|
||||
|
||||
Result values: `native` (ships its own `.d.ts`), `@types/<pkg>` (community types available), or `none` (auto-reject — un-typed library shifts maintenance cost to the feature).
|
||||
|
||||
### Filter 3: shadow-check
|
||||
|
||||
Check whether this library duplicates a must-have already locked in the workspace. Locked must-haves: `zod` (validation), `inversify` (DI, ADR-002), `payload` (CMS), `@trpc/server` (API layer), `superjson` (serialisation), `reflect-metadata` (DI metadata).
|
||||
|
||||
Command: `cat package.json | grep -E '"(zod|inversify|payload|@trpc/server|superjson|reflect-metadata)"'` — run from the workspace root.
|
||||
|
||||
Result values: `pass` (no shadow), `fail` (exact duplicate of a locked dep), `"shadows <x>"` (functional parallel that would create two libraries doing the same job — auto-reject). A replacement must be a separate ADR with consequences analysis, not a parallel adoption.
|
||||
|
||||
### Filter 4: boundary-fit
|
||||
|
||||
Confirm the dependency does not violate ESLint boundary-tag rules for the target tier (ADR-006, ADR-010, ADR-017).
|
||||
|
||||
Key rules:
|
||||
|
||||
- Feature packages cannot import `@sentry/*` or `@opentelemetry/sdk-*` directly — those are reserved for core (ADR-017 §4).
|
||||
- No package may import across feature boundaries without going through the event bus or tRPC.
|
||||
- Optional core packages can only be imported by apps and `core-composition`-tagged packages.
|
||||
|
||||
Check by reviewing what the proposed library's transitive imports would bring in and whether any violate the boundary ruleset.
|
||||
|
||||
Result values: `pass` or `fail`.
|
||||
|
||||
---
|
||||
|
||||
After Phase 1: tally results. If **any cheap filter failed**, the overall decision is `rejected`. Proceed to Phase 2 anyway — all expensive filters still run if the Phase 1 decision is already rejected (they inform the full record). If all cheap filters passed, proceed to Phase 2 to determine the final decision.
|
||||
|
||||
## Phase 2 — Expensive filters (short-circuit after first reject)
|
||||
|
||||
Run in order. On the first failure, set remaining filter results to `skip` and skip to the [Trace write step](#trace-write-step).
|
||||
|
||||
### Filter 5: maintenance
|
||||
|
||||
Check last release date and recent PR/issue activity:
|
||||
|
||||
```
|
||||
npm info <pkg> time.modified
|
||||
npm info <pkg> time | tail -5
|
||||
```
|
||||
|
||||
Result values:
|
||||
|
||||
- `active` — last release < 18 months **and** PR/issue activity < 12 months
|
||||
- `dormant` — stable, not actively developed (acceptable for finished libraries like `reflect-metadata`)
|
||||
- `abandoned` — last release ≥ 18 months **or** no activity in ≥ 12 months → auto-reject; short-circuit remaining expensive filters
|
||||
|
||||
On `abandoned` → set `cve-scan`, `eu-residency`, `named-consumer`, `socketRisk` to `skip` → write trace.
|
||||
|
||||
### Filter 6: cve-scan
|
||||
|
||||
```
|
||||
pnpm audit --audit-level=moderate 2>&1 | head -40
|
||||
```
|
||||
|
||||
Result values: `clean` (no advisories), an advisory ID like `GHSA-xxxx-xxxx-xxxx` (accepted risk — document in `accepted-cves` frontmatter), or `fail` (open advisory not accepted → auto-reject; short-circuit remaining expensive filters).
|
||||
|
||||
On `fail` → set `eu-residency`, `named-consumer`, `socketRisk` to `skip` → write trace.
|
||||
|
||||
### Filter 7: eu-residency
|
||||
|
||||
Applies only if the library transmits user data, telemetry, business state, or secrets to a vendor-controlled endpoint by default. Examples: analytics SDKs, error-tracking clients, AI APIs, log aggregation services.
|
||||
|
||||
Exemptions (result: `n/a`): pure in-process libraries (no network calls), self-hostable software where the operator controls the endpoint, and build-time-only tools.
|
||||
|
||||
For non-exempt libraries: verify the vendor offers an EU data region AND that the integration in `target` is configured to use it.
|
||||
|
||||
Result values: `ok` (vendor offers EU region, integration configured), `n/a` (no data transmission), `self-hostable` (operator-controlled endpoint), `fail` → auto-reject; short-circuit `named-consumer`.
|
||||
|
||||
On `fail` → set `named-consumer`, `socketRisk` to `skip` → write trace.
|
||||
|
||||
### Filter 8: named-consumer
|
||||
|
||||
Answer: **Who calls this code path today, or who is blocked waiting for it?**
|
||||
|
||||
A named consumer is a concrete call site that exists now or a feature blocked on this capability today. "We might want this later", "external clients could use this", and "it would be nice to have" are not named consumers.
|
||||
|
||||
If the only possible callers are hypothetical or future → `fail` → set `socketRisk` to `skip` → auto-reject.
|
||||
|
||||
Result value: `pass` or `fail`.
|
||||
|
||||
### Filter 9: supply-chain behavior (Socket)
|
||||
|
||||
**Expensive — network call. Run last in Phase 2. Short-circuit: if any earlier Phase 2 filter already rejected the library, set `socketRisk` to `skip` and proceed to the [Trace write step](#trace-write-step).**
|
||||
|
||||
Verify the package's supply-chain health via `socket-cli`:
|
||||
|
||||
```
|
||||
npx socket-cli@latest scan . --json 2>&1
|
||||
```
|
||||
|
||||
This scans the current directory's lockfile for packages installed from the target under evaluation. For a targeted single-package check before installing:
|
||||
|
||||
```
|
||||
npx socket-cli@latest info <pkg>@<version> --json 2>&1
|
||||
```
|
||||
|
||||
The JSON output contains an array of findings, each with a `severity` field. Cross-reference with the repo-root `.socket.json` `issueRules` to determine the classification:
|
||||
|
||||
| Finding severity | `.socket.json` rule | `socketRisk` value |
|
||||
| ----------------------------------- | ------------------- | ------------------- |
|
||||
| No findings, or only `medium`/`low` | `ignore` | `clean` |
|
||||
| `high`-severity finding present | `warn` | `flagged` |
|
||||
| `critical`-severity finding present | `error` | `<finding-summary>` |
|
||||
|
||||
Where `<finding-summary>` is a concise label for the critical finding (e.g. `"new-author-on-publish"`, `"install-scripts-added"`, `"exfiltrates-env"`).
|
||||
|
||||
Set `filter-results.socketRisk` in the trace frontmatter to one of these three values.
|
||||
|
||||
Result values:
|
||||
|
||||
- `clean` — no meaningful supply-chain signals; proceed to Phase 3 prompts.
|
||||
- `flagged` — `high`-severity finding; document the specific signal in the trace body and decide whether to accept with justification. Not an auto-reject.
|
||||
- `<finding-summary>` — `critical`-severity finding; auto-reject. This is the last filter — no further filters to skip.
|
||||
|
||||
---
|
||||
|
||||
## Skip sentinel
|
||||
|
||||
When a filter is short-circuited (not evaluated), write `skip` for its frontmatter value. The Zod schema validates approved traces end-to-end; rejected/partial traces may carry `skip` in fields that would normally require an enum value. The pre-commit check only validates that approved traces exist for new deps — partial traces are informational records.
|
||||
|
||||
## Three discussion prompts
|
||||
|
||||
Answer all three in the trace, regardless of filter outcome. These are not auto-reject filters; any answer is acceptable with justification.
|
||||
|
||||
### Prompt: replaces
|
||||
|
||||
What existing library or approach does this replace? New-and-old running in parallel is a smell — name the thing being retired and the retirement plan, or explain why parallel adoption is intentional and time-bounded.
|
||||
|
||||
### Prompt: migration-cost-out
|
||||
|
||||
What does ripping this back out look like 18 months from now? Rate: **mechanical** (swap one package, update call sites), **hard** (scattered integration points, data-format dependencies), or **impossible** (vendor lock-in, protocol coupling). Higher cost raises the bar for adoption.
|
||||
|
||||
### Prompt: alternatives-considered
|
||||
|
||||
Name at least two alternatives evaluated before choosing this library. For `core`-tier adoptions, this section is also duplicated into the companion ADR. If no alternatives exist, explain why (e.g., the library is the de-facto standard with no viable substitutes).
|
||||
|
||||
---
|
||||
|
||||
## Sub-processor classification
|
||||
|
||||
Answer these two questions before writing the trace. The answers become
|
||||
top-level frontmatter fields required by ADR-022 §9.
|
||||
|
||||
### Question: is-sub-processor
|
||||
|
||||
**Does the vendor receive personal data on the operator's behalf?**
|
||||
|
||||
A library is a sub-processor when it transmits personal data (user identifiers,
|
||||
email addresses, behavioural events, request bodies, etc.) to a vendor-controlled
|
||||
endpoint — analytics SDKs, error-tracking clients, AI APIs, log aggregation
|
||||
services. Network calls alone do not make a library a sub-processor; only calls
|
||||
that carry personal data do.
|
||||
|
||||
Pure in-process libraries (no network calls), self-hostable software where the
|
||||
operator controls the endpoint, and build-time-only tools are **not** sub-processors.
|
||||
|
||||
Set `is-sub-processor: true | false`.
|
||||
|
||||
### Question: processes-pii
|
||||
|
||||
**Does the library process personal data in-process, even without transmitting
|
||||
it to a vendor?**
|
||||
|
||||
A library processes PII if it reads, validates, serialises, stores, or
|
||||
transforms data fields that may contain personal information (names, emails,
|
||||
IDs, content authored by users, authentication credentials). A self-hosted
|
||||
database or CMS is a prime example: no data leaves to a vendor, yet the
|
||||
library clearly handles PII.
|
||||
|
||||
Pure utility libraries (DI containers, type validators, serialisers operating
|
||||
on already-typed objects without inspecting field semantics, test runners)
|
||||
typically answer `false`.
|
||||
|
||||
Set `processes-pii: true | false`.
|
||||
|
||||
### Conditional block: when is-sub-processor is true
|
||||
|
||||
When `is-sub-processor: true`, five additional fields are **required** in the
|
||||
trace frontmatter. Gather them before writing the trace:
|
||||
|
||||
```
|
||||
data-sent: "<what personal data the library transmits to the vendor>"
|
||||
region: "<vendor data region, e.g. eu-west-1 or eu>"
|
||||
dpa-signed: true | false # has the operator signed a DPA with this vendor?
|
||||
sccs-required: true | false # does the vendor require SCCs (non-EEA transfer)?
|
||||
contact: "<vendor DPO or privacy contact email/URL>"
|
||||
```
|
||||
|
||||
If the vendor does not yet have a signed DPA or if you cannot determine the
|
||||
region, record `dpa-signed: false` / region as best-known and add a prose note
|
||||
under `## Sub-processor` in the trace body explaining the gap.
|
||||
|
||||
---
|
||||
|
||||
## Trace write step
|
||||
|
||||
Write the trace **unconditionally** at evaluation end — even for rejections, even for partial traces.
|
||||
|
||||
**Path:** `docs/library-decisions/<YYYY-MM-DD>-<package-name>.md`
|
||||
|
||||
Use today's date. Use `docs/library-decisions/_template.md` as the structural guide.
|
||||
|
||||
Frontmatter rules:
|
||||
|
||||
- `decision: approved` only if all eight filters passed. Otherwise `decision: rejected`.
|
||||
- `adr: null` for feature-tier. For core-tier approvals, coordinate the ADR slug before writing (`adr: adr-NNN`).
|
||||
- `verification-commands` — include the literal commands run for each filter, one per line.
|
||||
- `accepted-cves: []` (empty unless you accepted a specific advisory).
|
||||
- `is-sub-processor` and `processes-pii` are **always required** (see Sub-processor classification above).
|
||||
- When `is-sub-processor: true`, include `data-sent`, `region`, `dpa-signed`, `sccs-required`, and `contact`.
|
||||
- For skipped expensive filters, write `skip` for the frontmatter value and omit the prose section body or note "Not evaluated — skipped due to earlier rejection."
|
||||
|
||||
Frontmatter template:
|
||||
|
||||
```yaml
|
||||
---
|
||||
package: <name>
|
||||
version: "<semver range>"
|
||||
tier: app | feature | core
|
||||
decision: approved | rejected
|
||||
date: <YYYY-MM-DD>
|
||||
deciders: [<author>, ...]
|
||||
adr: adr-NNN | null
|
||||
lastRevalidated: null
|
||||
is-sub-processor: false
|
||||
processes-pii: false
|
||||
# include the block below only when is-sub-processor: true
|
||||
# data-sent: "<description>"
|
||||
# region: "<eu | eu-west-1 | ...>"
|
||||
# dpa-signed: false
|
||||
# sccs-required: false
|
||||
# contact: "<url or email>"
|
||||
filter-results:
|
||||
license: <SPDX id>
|
||||
types: native | "@types/<x>" | none
|
||||
maintenance: active | dormant | abandoned
|
||||
boundary-fit: pass | fail
|
||||
shadow-check: pass | fail | "shadows <x>"
|
||||
eu-residency: ok | n/a | self-hostable | fail
|
||||
cve-scan: clean | "<advisory-id>" | fail
|
||||
named-consumer: pass | fail
|
||||
socketRisk: clean | flagged | <arbitrary-string>
|
||||
verification-commands:
|
||||
- <literal command that produced each filter result>
|
||||
accepted-cves: []
|
||||
---
|
||||
```
|
||||
|
||||
After writing the trace:
|
||||
|
||||
- For approved traces: confirm the trace is staged in the same commit as the `package.json` change. The pre-commit hook validates this.
|
||||
- For rejected traces: stage the trace file alone. Do not run `pnpm add <pkg>`.
|
||||
|
||||
</runbook>
|
||||
|
||||
<output-format>
|
||||
|
||||
After completing the evaluation, emit a one-paragraph summary:
|
||||
|
||||
```
|
||||
/evaluate-library result: <approved|rejected> — <package>@<version> (<tier>)
|
||||
Rejection filters (if any): <filter names>
|
||||
Trace written to: docs/library-decisions/<date>-<package>.md
|
||||
```
|
||||
|
||||
</output-format>
|
||||
167
.claude/skills/evaluate-library/TRACE-TEMPLATE.md
Normal file
167
.claude/skills/evaluate-library/TRACE-TEMPLATE.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Trace Template
|
||||
|
||||
Use this file as the structural guide when writing a library decision trace. Copy the frontmatter block and all 11 headings. Replace placeholder values with real results.
|
||||
|
||||
Trace path: `docs/library-decisions/<YYYY-MM-DD>-<package-name>.md`
|
||||
|
||||
---
|
||||
|
||||
## Frontmatter — all filters evaluated
|
||||
|
||||
```markdown
|
||||
---
|
||||
package: <npm-package-name>
|
||||
version: "<semver range>"
|
||||
tier: app | feature | core
|
||||
decision: approved | rejected
|
||||
date: <YYYY-MM-DD>
|
||||
deciders: [<author>, ...]
|
||||
adr: adr-NNN | null
|
||||
filter-results:
|
||||
license: <SPDX id>
|
||||
types: native | "@types/<x>" | none
|
||||
maintenance: active | dormant | abandoned
|
||||
boundary-fit: pass | fail
|
||||
shadow-check: pass | fail | "shadows <x>"
|
||||
eu-residency: ok | n/a | self-hostable | fail
|
||||
cve-scan: clean | "<advisory-id>" | fail
|
||||
named-consumer: pass | fail
|
||||
socketRisk: clean | flagged | <finding-summary>
|
||||
verification-commands:
|
||||
- <literal command that produced the license result>
|
||||
- <literal command that confirmed types>
|
||||
- <literal command that checked maintenance>
|
||||
- <literal command that ran the CVE scan>
|
||||
accepted-cves: []
|
||||
---
|
||||
```
|
||||
|
||||
## Frontmatter — partial trace (expensive filters short-circuited)
|
||||
|
||||
When an expensive filter fails (Phase 2 short-circuit), set remaining filter fields to `skip`. The Zod schema validates approved traces end-to-end; `skip` is the accepted sentinel for unevaluated fields in rejected traces.
|
||||
|
||||
Example: `maintenance: abandoned` → `cve-scan`, `eu-residency`, `named-consumer` skipped.
|
||||
|
||||
```markdown
|
||||
---
|
||||
package: <npm-package-name>
|
||||
version: "<semver range>"
|
||||
tier: feature | core
|
||||
decision: rejected
|
||||
date: <YYYY-MM-DD>
|
||||
deciders: [<author>, ...]
|
||||
adr: null
|
||||
filter-results:
|
||||
license: MIT
|
||||
types: native
|
||||
maintenance: abandoned
|
||||
boundary-fit: pass
|
||||
shadow-check: pass
|
||||
eu-residency: skip
|
||||
cve-scan: skip
|
||||
named-consumer: skip
|
||||
socketRisk: skip
|
||||
verification-commands:
|
||||
- npm view <pkg> time.modified
|
||||
accepted-cves: []
|
||||
---
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Required headings (11 total, in this order)
|
||||
|
||||
### Filter sections (8)
|
||||
|
||||
```markdown
|
||||
## Filter: license
|
||||
|
||||
<!-- Result: <SPDX id> -->
|
||||
|
||||
Record the SPDX identifier from `package.json` or `npx license-checker --packages <pkg>`.
|
||||
Allowed: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, MPL-2.0.
|
||||
Anything else → auto-reject (note the identifier and rejection reason).
|
||||
|
||||
## Filter: types
|
||||
|
||||
<!-- Result: native | @types/<x> | none -->
|
||||
|
||||
Confirm TypeScript types are available. `native` = ships its own `.d.ts`; `@types/<x>` = community
|
||||
types package exists and is current; `none` = no types → auto-reject.
|
||||
|
||||
## Filter: maintenance
|
||||
|
||||
<!-- Result: active | dormant | abandoned -->
|
||||
|
||||
Check last release date and recent PR/issue activity. `active` = last release < 18 months AND
|
||||
activity < 12 months. `dormant` = stable but not actively developed (acceptable for finished
|
||||
libraries). `abandoned` = auto-reject.
|
||||
If skipped (earlier expensive filter failed), write: "Not evaluated — skipped due to <filter> rejection."
|
||||
|
||||
## Filter: boundary-fit
|
||||
|
||||
<!-- Result: pass | fail -->
|
||||
|
||||
Confirm the dependency does not violate ESLint boundary-tag rules for the target tier
|
||||
(ADR-006, ADR-010, ADR-017). Name the specific rule checked and the result.
|
||||
|
||||
## Filter: shadow-check
|
||||
|
||||
<!-- Result: pass | fail | "shadows <x>" -->
|
||||
|
||||
Check whether this library duplicates a must-have already locked in the workspace.
|
||||
Locked must-haves: zod, inversify, payload, @trpc/server, superjson, reflect-metadata.
|
||||
`shadows <x>` → auto-reject; a replacement requires a dedicated ADR.
|
||||
|
||||
## Filter: eu-residency
|
||||
|
||||
<!-- Result: ok | n/a | self-hostable | fail | skip -->
|
||||
|
||||
If the library transmits user data, telemetry, or business state to a vendor-controlled
|
||||
endpoint by default, the vendor must offer an EU data region and the integration must be
|
||||
configured to use it. Pure in-process libraries and build-time tools → `n/a`.
|
||||
If skipped, write: "Not evaluated — skipped due to <filter> rejection."
|
||||
|
||||
## Filter: cve-scan
|
||||
|
||||
<!-- Result: clean | "<advisory-id>" | fail | skip -->
|
||||
|
||||
Run `pnpm audit --audit-level=moderate`. `clean` = no advisories at adoption time. Record
|
||||
accepted advisory IDs in the `accepted-cves` frontmatter field and explain the risk acceptance
|
||||
here. If skipped, write: "Not evaluated — skipped due to <filter> rejection."
|
||||
|
||||
## Filter: named-consumer
|
||||
|
||||
<!-- Result: pass | fail | skip -->
|
||||
|
||||
Answer: "Who calls this code path today, or who is blocked waiting for it?"
|
||||
Hypothetical future callers are not consumers (ADR-022 §2.8 — the direct response to the
|
||||
2026-05-14 OpenAPI near-miss). If skipped, write: "Not evaluated — skipped due to <filter> rejection."
|
||||
```
|
||||
|
||||
### Prompt sections (3)
|
||||
|
||||
```markdown
|
||||
## Prompt: replaces
|
||||
|
||||
<!-- Required: answer in either direction with justification -->
|
||||
|
||||
What existing library or approach does this replace? New-and-old running in parallel is a smell.
|
||||
Name the thing being retired and its retirement plan, or explain why parallel adoption is
|
||||
intentional and time-bounded.
|
||||
|
||||
## Prompt: migration-cost-out
|
||||
|
||||
<!-- Required: mechanical | hard | impossible + justification -->
|
||||
|
||||
What does ripping this back out look like 18 months from now? Rate: mechanical (swap package,
|
||||
update call sites), hard (scattered integration, data-format dependencies), or impossible
|
||||
(vendor lock-in, protocol coupling). Higher migration cost raises the adoption bar.
|
||||
|
||||
## Prompt: alternatives-considered
|
||||
|
||||
<!-- Required: minimum two named alternatives, or "none with explanation" -->
|
||||
|
||||
Name at least two alternatives evaluated before choosing this library. For core-tier adoptions,
|
||||
this section is also duplicated into the companion ADR. If no alternatives exist, explain why.
|
||||
```
|
||||
18
.claude/skills/grill-me/SKILL.md
Normal file
18
.claude/skills/grill-me/SKILL.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: grill-me
|
||||
description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when the user wants to stress-test a plan, get grilled, or mentions "grill me". Use grill-with-docs instead when the plan should cross-check against ADRs + glossary + manifests.
|
||||
---
|
||||
|
||||
Interview the user relentlessly about every aspect of this plan until you reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
|
||||
|
||||
Ask the questions **one at a time**, waiting for feedback before continuing.
|
||||
|
||||
If a question can be answered by exploring the codebase, explore the codebase instead. Useful shortcuts in this repo:
|
||||
|
||||
- `pnpm work status` — current epics and ready stories
|
||||
- `cat packages/<feature>/src/feature.manifest.ts` — declared use cases / events / audits
|
||||
- `ls docs/decisions/` — ADRs by number
|
||||
- `pnpm fallow` — dead exports, dupes, complexity hotspots
|
||||
- grep manifests across all features: `grep -r "publishes:" packages/*/src/feature.manifest.ts`
|
||||
|
||||
When grilling pulls in ADR / glossary / manifest cross-checks and you want to update those docs inline, switch to `grill-with-docs` instead.
|
||||
106
.claude/skills/grill-with-docs/SKILL.md
Normal file
106
.claude/skills/grill-with-docs/SKILL.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
name: grill-with-docs
|
||||
description: Stress-test a plan against this repo's domain glossary, ADRs, conformance rules, and feature manifests. Update docs/glossary.md inline as terms crystallize; offer ADRs sparingly. Use when the user wants to harden a plan before it becomes a PRD.
|
||||
---
|
||||
|
||||
<what-to-do>
|
||||
|
||||
Interview the user relentlessly about every aspect of this plan until you reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
|
||||
|
||||
Ask the questions **one at a time**, waiting for feedback on each before continuing.
|
||||
|
||||
If a question can be answered by exploring the codebase, explore the codebase instead of asking. The repo has fast feedback loops — run `pnpm work status`, grep manifests, read feature `feature.manifest.ts`, check ADRs. Speculation is a last resort.
|
||||
|
||||
</what-to-do>
|
||||
|
||||
<supporting-info>
|
||||
|
||||
## Repo doc map
|
||||
|
||||
This repo uses a **single context** with these doc locations:
|
||||
|
||||
```
|
||||
/
|
||||
├── docs/
|
||||
│ ├── glossary.md ← lazy-create when first term resolves
|
||||
│ ├── decisions/ ← ADRs (adr-NNN-<slug>.md, 3-digit zero-pad)
|
||||
│ │ ├── adr-001-monorepo-tool.md
|
||||
│ │ ├── adr-018-audit-and-compliance.md
|
||||
│ │ └── ...
|
||||
│ ├── architecture/ ← long-form specs + workflow design
|
||||
│ ├── work/ ← PRDs, epics, stories, tasks
|
||||
│ └── guides/ ← how-to runbooks
|
||||
└── packages/<feature>/src/feature.manifest.ts ← per-feature contract
|
||||
```
|
||||
|
||||
There is **no `CONTEXT.md` or `CONTEXT-MAP.md`** — this repo uses `docs/glossary.md` (create lazily) plus the per-feature `feature.manifest.ts` files for machine-readable domain shape. Don't create the multi-context layout (`CONTEXT-MAP.md`) unless this becomes a polyrepo.
|
||||
|
||||
## During the session
|
||||
|
||||
### Challenge against the glossary + manifests
|
||||
|
||||
When the user introduces a term that conflicts with `docs/glossary.md` (if it exists) or with a `feature.manifest.ts` entry, call it out immediately:
|
||||
|
||||
> "Your glossary defines `cancellation` as the act of voiding an unsent invoice, but you seem to mean the user-initiated subscription teardown — which is it?"
|
||||
|
||||
> "`auth.signIn` exists in `packages/auth/src/feature.manifest.ts` with that exact slug — are you adding a new use case or extending the existing one?"
|
||||
|
||||
### Sharpen fuzzy language
|
||||
|
||||
When the user uses vague or overloaded terms, propose a precise canonical term:
|
||||
|
||||
> "You're saying `account` — do you mean a `User` (entity in `packages/auth`) or a Payload-collection record? Those are distinct."
|
||||
|
||||
### Discuss concrete scenarios
|
||||
|
||||
When domain relationships are being discussed, stress-test them with specific scenarios. Invent edge cases that force precision about boundaries between concepts. Lean on the existing feature set — auth, blog, media, marketing-pages, navigation — for grounding examples.
|
||||
|
||||
### Cross-reference with code
|
||||
|
||||
When the user states how something works, verify it against the code. Look at:
|
||||
|
||||
- The feature's `feature.manifest.ts` for declared use cases, audits, publishes, consumes
|
||||
- `packages/<feature>/src/application/use-cases/` for the actual shape
|
||||
- `packages/<feature>/src/di/bind-production.ts` for what's wired
|
||||
- `docs/decisions/adr-NNN-*.md` for the decision history
|
||||
|
||||
If you find a contradiction, surface it:
|
||||
|
||||
> "You said cross-feature reactions happen through the bus, but `packages/auth/src/feature.manifest.ts` shows `publishes: []` — has this been wired yet?"
|
||||
|
||||
### Cross-reference with ADRs
|
||||
|
||||
Before recommending an approach, scan `docs/decisions/` for relevant ADRs. If your recommendation contradicts a current-status ADR, surface that explicitly:
|
||||
|
||||
> "You're proposing direct cross-feature imports, but `adr-006-vertical-feature-packages.md` plus rule R20 in the ESLint config forbid that — events (`core-events`) are the sanctioned path. Want to use events, or do you want to reopen the ADR?"
|
||||
|
||||
### Cross-reference with conformance rules
|
||||
|
||||
The conformance system (`docs/architecture/agent-first-workflow-and-conformance.md`) defines hard contracts:
|
||||
|
||||
- Every use case has a manifest entry → contracts → tests → impl (in that order)
|
||||
- TS brands (`Instrumented`, `Captured`, `Audited`) attached at DI bind time
|
||||
- ESLint rules enforce manifest ↔ code alignment
|
||||
- `pnpm conformance` enforces cross-feature event closure
|
||||
|
||||
If the plan would violate any of these, flag it.
|
||||
|
||||
### Update `docs/glossary.md` inline
|
||||
|
||||
When a term is resolved during the conversation, append it to `docs/glossary.md` right then — don't batch. Lazy-create the file when the first term is resolved. Use the format in [glossary-format.md](./glossary-format.md).
|
||||
|
||||
Only include terms meaningful to **this repo's domain** (template / monorepo / agent-first workflow / clean architecture / vertical features). Skip general programming concepts. Skip implementation details — those belong in code or ADRs.
|
||||
|
||||
### Offer ADRs sparingly
|
||||
|
||||
Only offer to create an ADR when all three are true:
|
||||
|
||||
1. **Hard to reverse** — the cost of changing your mind later is meaningful
|
||||
2. **Surprising without context** — a future reader will wonder "why did they do it this way?"
|
||||
3. **Result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
|
||||
|
||||
If any is missing, skip the ADR. The repo's ADRs follow a long-form `Context → Decision → Alternatives considered → Consequences → Related` shape (see `docs/decisions/adr-015-events-and-jobs.md` for a representative example). Number is next-highest in `docs/decisions/` zero-padded to 3 digits (`adr-020-...`, `adr-021-...`).
|
||||
|
||||
If the grill produced a major refactor decision rather than a new feature, lead the user to an ADR; if it produced a feature plan, lead to a PRD via `to-prd`.
|
||||
|
||||
</supporting-info>
|
||||
62
.claude/skills/grill-with-docs/glossary-format.md
Normal file
62
.claude/skills/grill-with-docs/glossary-format.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# docs/glossary.md Format
|
||||
|
||||
## Structure
|
||||
|
||||
```md
|
||||
# Glossary
|
||||
|
||||
Domain vocabulary for `template-vertical`. Terms specific to this repo — clean architecture, vertical features, agent workflow, conformance. General programming concepts don't belong here; implementation details belong in code or ADRs.
|
||||
|
||||
## Architecture
|
||||
|
||||
**Feature**:
|
||||
A vertical slice owning its Clean Architecture layers (entities → application → infrastructure → DI → integrations).
|
||||
_Avoid_: module, domain, app.
|
||||
|
||||
**Use case**:
|
||||
A single business action exposed by a feature, implemented as a factory `(deps) => async (input) => output`. Each one has a manifest entry, a Zod input/output schema pair, a colocated test, and a controller.
|
||||
_Avoid_: command, action, handler.
|
||||
|
||||
**Manifest**:
|
||||
The `feature.manifest.ts` file that declares a feature's use cases, audits, publishes, consumes, and required core packages. Source of truth for conformance gates.
|
||||
|
||||
**Conformance**:
|
||||
The 5-gate enforcement system (TS brands → ESLint → boot assertion → `pnpm conformance` → fallow) that keeps manifest and code aligned.
|
||||
|
||||
## Workflow
|
||||
|
||||
**PRD**:
|
||||
The top-level requirements doc at `docs/work/prds/<date>-<slug>.prd.md` that seeds an epic.
|
||||
|
||||
**Epic**:
|
||||
A large body of work containing stories. Folder at `docs/work/epics/<epic-slug>/_epic.md`.
|
||||
|
||||
**Story**:
|
||||
One use case or technical capability. Folder under the epic, file `_story.md`.
|
||||
|
||||
**Task**:
|
||||
One vertical slice = one PR = one commit. File `<slug>.task.md` under the story folder.
|
||||
|
||||
## Relationships
|
||||
|
||||
- A **PRD** decomposes into one or more **Epics**
|
||||
- An **Epic** contains one or more **Stories**
|
||||
- A **Story** is implemented by one or more **Tasks**
|
||||
- A **Use case** is declared in a **Manifest** before it has code
|
||||
- **Conformance** asserts that the **Manifest** and the code agree
|
||||
|
||||
## Flagged ambiguities
|
||||
|
||||
- (none yet — append here when conflicts are resolved during grilling)
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others as aliases to avoid.
|
||||
- **Flag conflicts explicitly.** When grilling surfaces ambiguity, capture both meanings under "Flagged ambiguities" with the resolution.
|
||||
- **Keep definitions tight.** One sentence max. Define what it IS, not what it does.
|
||||
- **Show relationships.** Use bold term names; express cardinality where obvious.
|
||||
- **Only domain terms specific to this repo.** General programming concepts (timeouts, retries, errors, DI) don't belong even if used heavily. Before adding, ask: is this concept unique to template-vertical, or generic?
|
||||
- **Group under subheadings** when natural clusters emerge (Architecture, Workflow, Instrumentation, etc.).
|
||||
|
||||
This repo is **single-context**: one `docs/glossary.md`, no `CONTEXT-MAP.md`. Don't switch to multi-context layout unless the repo splits.
|
||||
36
.claude/skills/handoff/SKILL.md
Normal file
36
.claude/skills/handoff/SKILL.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: handoff
|
||||
description: Compact the current conversation into a handoff document for another agent to pick up. Use when the user wants to transition work to a fresh session, switch worktrees, or hand off to a subagent.
|
||||
argument-hint: "What will the next session be used for?"
|
||||
---
|
||||
|
||||
Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save it to a path produced by `mktemp -t handoff-XXXXXX.md` (read the file before you write to it).
|
||||
|
||||
Suggest the skills the next session should use, if any. In this repo, the common follow-ups are:
|
||||
|
||||
- `grill-with-docs` — stress-test the plan before coding
|
||||
- `to-prd` — materialize the plan into `docs/work/prds/<date>-<slug>.prd.md`
|
||||
- `superpowers:writing-plans` — author the implementation plan
|
||||
- `superpowers:subagent-driven-development` — dispatch implementer + reviewer subagents per task
|
||||
|
||||
## Don't duplicate
|
||||
|
||||
Reference these artifacts by path or URL rather than inlining their content:
|
||||
|
||||
- PRDs (`docs/work/prds/*.prd.md`), epics (`docs/work/epics/<epic>/_epic.md`), stories (`_story.md`), tasks (`*.task.md`)
|
||||
- ADRs (`docs/decisions/adr-NNN-*.md`)
|
||||
- AGENTS.md and CLAUDE.md (the next agent loads these automatically)
|
||||
- `_state.json` (orchestrator-derived; the next agent regenerates it from markdown via `pnpm work rebuild-state`)
|
||||
- Commit messages, diffs, PR descriptions — link the SHA / PR number
|
||||
- Existing plans under `docs/superpowers/plans/`
|
||||
|
||||
## Do capture
|
||||
|
||||
- The active **goal** in one sentence
|
||||
- **In-flight branch / worktree** and any uncommitted state (e.g. `git status` summary, dangling commits)
|
||||
- **Decisions made in conversation** that haven't yet landed in a PRD or ADR
|
||||
- **Blockers** and proposed next steps
|
||||
- **Skills to invoke first** in the next session
|
||||
- If the user passed arguments, treat them as the next session's focus and tailor the doc accordingly
|
||||
|
||||
Keep the document short — it's a baton, not a thesis.
|
||||
98
.claude/skills/improve-codebase-architecture/DEEPENING.md
Normal file
98
.claude/skills/improve-codebase-architecture/DEEPENING.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Deepening
|
||||
|
||||
How to deepen a cluster of shallow modules in this repo, given its dependencies. **In this repo "module" defaults to "feature"** — most deepenings operate on or within a feature (`packages/<name>/`). Narrower scopes (use case, controller, repository) follow the same dependency-category logic. Assumes the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**.
|
||||
|
||||
## Dependency categories
|
||||
|
||||
When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam.
|
||||
|
||||
### 1. In-process
|
||||
|
||||
Pure computation, in-memory state, no I/O. Always deepenable — merge the modules and test through the new interface directly. No adapter needed.
|
||||
|
||||
**Where this lives in our repo:**
|
||||
|
||||
- `entities/models/**` — Zod schemas + types + pure helpers
|
||||
- `entities/errors/**` — domain error classes
|
||||
- `application/use-cases/**` — pure orchestration (factories take ports as deps)
|
||||
- `interface-adapters/controllers/**` + their colocated `presenter` functions
|
||||
- Pure helpers in `core-shared/conformance/`, `core-shared/instrumentation/` (the non-OTel parts)
|
||||
|
||||
For category 1 modules: **merge, then test the result through its interface**. No mocks. The test surface IS the new interface.
|
||||
|
||||
### 2. Local-substitutable
|
||||
|
||||
Dependencies that have local test stand-ins. **In this repo every infrastructure port already has both a real implementation and a mock side-by-side** — the mock IS the test stand-in.
|
||||
|
||||
**Pattern (from ADR-012):**
|
||||
|
||||
- `<x>.repository.interface.ts` — the port (seam)
|
||||
- `<x>.repository.ts` — Payload-backed adapter (real)
|
||||
- `<x>.repository.mock.ts` — in-memory adapter (test stand-in)
|
||||
|
||||
The deepened module is tested with the `.mock.ts` adapter injected directly via the factory function. No container rebinding (ADR-012).
|
||||
|
||||
**Common category-2 ports in this repo:**
|
||||
|
||||
- `IUsersRepository`, `IArticlesRepository`, `IMediaRepository`, etc. → Mock + Payload adapters
|
||||
- `IAuthenticationService` → Mock + real adapter
|
||||
- `IJobQueue` (from `core-shared/jobs/`) → `InMemoryJobQueue` + `PayloadJobQueue`
|
||||
- `IEventBus` (from `core-events/`) → `InMemoryEventBus` + `PayloadJobsEventBus`
|
||||
- `ITracer`, `ILogger`, `IMetrics` (from `core-shared/instrumentation/`) → `Noop*` + `Otel*` + `Recording*` (the third one lives in `core-testing` for assertions)
|
||||
|
||||
If the deepening touches a category-2 port, the recommendation shape is: _"Merge X into Y, keep the port boundary at the existing `<x>.repository.interface.ts`; both adapters survive unchanged."_
|
||||
|
||||
### 3. Remote but owned
|
||||
|
||||
Our own services across a network boundary. **In this repo, cross-feature communication is already this pattern via the event bus (ADR-015).**
|
||||
|
||||
The **port** is `IEventBus.publish(descriptor, payload)` + `IEventBus.subscribe(descriptor, consumerFeature, handler)`. Adapters:
|
||||
|
||||
- `InMemoryEventBus` (test + dev-seed) — synchronous fan-out
|
||||
- `PayloadJobsEventBus` (production) — Payload tasks fan-out durably across the network if features are split into separate deploys
|
||||
|
||||
If a deepening proposal would introduce a NEW cross-feature seam, the answer is almost always "use the event bus" — don't invent a new transport. Rule E0 forbids in-feature use of the bus (in-feature reactions are direct use-case calls); E1 keeps consumer handlers private.
|
||||
|
||||
If the deepening proposal would EXPOSE one feature's internals to another, that's a boundary violation — reject and suggest events instead.
|
||||
|
||||
### 4. True external
|
||||
|
||||
Third-party services we don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter.
|
||||
|
||||
**Where this lives in our repo:**
|
||||
|
||||
- Payload CMS itself — features depend on `IXRepository`, not on `payload` directly. The real adapter (`<x>.repository.ts`) is the only place Payload is touched.
|
||||
- Sentry / OpenTelemetry exporters — features use `ITracer`/`ILogger`/`IMetrics` from `core-shared/instrumentation/`. The OTel SDK lives ONLY in `core-shared/instrumentation/otel/` (ESLint-enforced via rule R52).
|
||||
- Socket.IO — features use `IRealtimeBroadcaster`; `socket.io` itself lives only in `@repo/core-realtime` (rule R2).
|
||||
|
||||
If a deepening proposal would import a vendor SDK from a feature package, that's an ADR-014/ADR-016/ADR-017 violation — reject and route through the existing port.
|
||||
|
||||
## Seam discipline
|
||||
|
||||
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). Single-adapter ports in this repo are usually a smell — check if the mock is missing or if the port itself is unnecessary.
|
||||
- **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them.
|
||||
- **The DI symbol is the seam contract.** `*_SYMBOLS.IXRepository` plus `<x>.repository.interface.ts` together define what callers depend on. Adapter swaps happen at bind time in `bind-production.ts` / `bind-dev-seed.ts`.
|
||||
- **`feature.manifest.ts` is the structural-conformance seam.** If a deepening moves use cases across features, the manifests of BOTH features change — the conformance ESLint rules + `assertFeatureConformance` boot check enforce that the move is reflected in declarations, not just code.
|
||||
|
||||
## Testing strategy: replace, don't layer
|
||||
|
||||
- **Old unit tests on shallow modules become waste** once tests at the deepened module's interface exist — delete them. Our coverage thresholds (ADR-020) reward this: collapsing N shallow modules + their N test files into one deep module + one test file at its interface keeps coverage 100% without test bloat.
|
||||
- **Write new tests at the deepened module's interface.** The **interface is the test surface**.
|
||||
- **Tests assert on observable outcomes through the interface**, not internal state.
|
||||
- **Tests should survive internal refactors** — if a test has to change when the implementation changes, it's testing past the interface.
|
||||
- **L1 diff coverage (`pnpm coverage:diff`) will surface uncovered lines after the refactor** — every deepened module needs its new tests to cover the merged behaviour before the refactor PR is mergeable.
|
||||
|
||||
## Conformance check (run before claiming the deepening is complete)
|
||||
|
||||
After deepening, the following must all stay green:
|
||||
|
||||
```
|
||||
pnpm typecheck # TS brand-slot enforcement
|
||||
pnpm lint # ESLint conformance + boundaries
|
||||
pnpm test --filter @repo/<feature> -- --coverage # per-layer L0 thresholds
|
||||
pnpm conformance # cross-feature event closure
|
||||
pnpm fallow:audit # whole-codebase audit + dead-export sweep
|
||||
pnpm coverage:diff -- --base origin/main # cover-the-diff (ADR-020 L1)
|
||||
```
|
||||
|
||||
If `pnpm dev` was running, it should still boot — `assertFeatureConformance` will fail loudly on brand-slot or manifest drift if the deepening forgot a binding update.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Interface Design
|
||||
|
||||
When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout) — your first idea is unlikely to be the best.
|
||||
|
||||
Uses the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module** (= **feature** by default in this repo), **interface**, **seam**, **adapter**, **leverage**.
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Frame the problem space
|
||||
|
||||
Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate:
|
||||
|
||||
- The constraints any new interface would need to satisfy
|
||||
- The dependencies it would rely on, and which category they fall into (see [DEEPENING.md](DEEPENING.md))
|
||||
- The **hard constraints from SKILL.md** that the new interface must respect (factory-function shape, per-feature DI, manifest-first, generator-first, brand wrappers, etc.)
|
||||
- A rough illustrative code sketch to ground the constraints — not a proposal, just a way to make the constraints concrete
|
||||
|
||||
Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel.
|
||||
|
||||
### 2. Spawn sub-agents
|
||||
|
||||
Spawn 3+ sub-agents in parallel using the `Agent` tool (`subagent_type=general-purpose` or a more specific type if appropriate). Each must produce a **radically different** interface for the deepened module.
|
||||
|
||||
Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint:
|
||||
|
||||
- **Agent 1**: "Minimise the interface — aim for 1–3 entry points max. Maximise leverage per entry point."
|
||||
- **Agent 2**: "Maximise flexibility — support many use cases and extension."
|
||||
- **Agent 3**: "Optimise for the most common caller — make the default case trivial."
|
||||
- **Agent 4 (if applicable)**: "Design around ports & adapters for cross-seam dependencies."
|
||||
|
||||
**Every brief MUST also include:**
|
||||
|
||||
- This repo's vocabulary from [`docs/glossary.md`](../../../docs/glossary.md) (use case, manifest, feature, slice, etc.)
|
||||
- The architecture vocabulary from [LANGUAGE.md](LANGUAGE.md)
|
||||
- The hard constraints from [SKILL.md](SKILL.md) — sub-agents must not propose interfaces that violate ADR-006 (boundaries), ADR-008 (per-feature DI), ADR-012 (factory shape, one controller per use case), ADR-013 (schemas in use-case file), ADR-014/017 (vendor isolation), ADR-015 (events for cross-feature), ADR-020 (manifest-driven coverage bands), or ADR-021 (versioning by commit-path).
|
||||
- The relevant feature `feature.manifest.ts` shape so the proposed interface aligns with manifest-first ordering.
|
||||
|
||||
Each sub-agent outputs:
|
||||
|
||||
1. **Interface** (types, methods, params — plus invariants, ordering, error modes, schemas if applicable)
|
||||
2. **Usage example** showing how callers in this repo would use it (use real file paths and real existing feature names)
|
||||
3. **What the implementation hides** behind the seam
|
||||
4. **Dependency strategy and adapters** (see [DEEPENING.md](DEEPENING.md)) — which existing ports/adapters get reused, which (if any) are new
|
||||
5. **Manifest + binder impact** — which `feature.manifest.ts` entries and which `bind-production.ts` / `bind-dev-seed.ts` files change
|
||||
6. **Trade-offs** — where leverage is high, where it's thin
|
||||
7. **ADR conflicts (if any)** — call out by ADR number with rationale, or state "none"
|
||||
|
||||
### 3. Present and compare
|
||||
|
||||
Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by:
|
||||
|
||||
- **Depth** (leverage at the interface)
|
||||
- **Locality** (where change concentrates)
|
||||
- **Seam placement** (which existing `*.interface.ts` survives, which gets replaced, which is new)
|
||||
- **Conformance impact** (how many manifests change, how many binders change, how many tests rewrite)
|
||||
- **Coverage delta** (cumulative L0 band impact — does any layer drop below its declared 100% / 95%?)
|
||||
|
||||
After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated — the user wants a strong read, not a menu.
|
||||
|
||||
If the chosen design crosses a feature-package boundary (e.g. moves a use case from `@repo/blog` to `@repo/media`), state explicitly:
|
||||
|
||||
- Which `feature.manifest.ts` files lose / gain entries
|
||||
- Which package versions will bump on the next release-please PR (per ADR-021 commit-path bump targeting)
|
||||
- Whether the migration needs an intermediate compatibility seam to keep `pnpm conformance` green during the transition
|
||||
|
||||
The implementation lands via the manifest-first ordering: (1) update the manifests in both packages, (2) write the new contracts in the use-case file, (3) write the failing tests, (4) implement until green. Don't skip the order even when the move feels mechanical.
|
||||
78
.claude/skills/improve-codebase-architecture/LANGUAGE.md
Normal file
78
.claude/skills/improve-codebase-architecture/LANGUAGE.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Language
|
||||
|
||||
Shared vocabulary for every suggestion this skill makes. Use these terms exactly — don't substitute "component," "service" (we use that narrowly for DI ports), "API," or "boundary" (overloaded with our workspace-tag enforcement). Consistent language is the whole point.
|
||||
|
||||
This vocabulary is foundational for the skill's reasoning. The project's domain vocabulary lives in [`docs/glossary.md`](../../../docs/glossary.md) — terms like _use case_, _manifest_, _slice_, _binder_, _brand_, _conformance band_, _coverage layer_. Both vocabularies are in scope when proposing deepenings; see the "Mapping to this repo's identifiers" section below for how the abstract terms here land on concrete file shapes.
|
||||
|
||||
## Terms
|
||||
|
||||
**Module** — **in this repo, "module" defaults to "feature"** (`packages/<name>/`). The abstract definition (anything with an interface + implementation) still applies at narrower scales — a use case, controller, repository/service port, or binder can also be a module — but **whenever the refactor scope is "the whole thing", say feature**. Reach for "module" only when the abstraction across scales actually matters (e.g., comparing how a use case's depth differs from its containing feature's depth).
|
||||
_Avoid_: unit, component, service (we use "service" for DI ports specifically).
|
||||
|
||||
**Interface**
|
||||
Everything a caller must know to use the module correctly. Includes the type signature, but also invariants, ordering constraints, error modes, required configuration, performance characteristics, **manifest declarations**, and **DI symbol contract**.
|
||||
_Avoid_: API, signature (too narrow — those refer only to the type-level surface).
|
||||
|
||||
**Implementation**
|
||||
What's inside a module — its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Payload-backed repository) or a large adapter with a small implementation (an in-memory mock). Reach for "adapter" when the seam is the topic; "implementation" otherwise.
|
||||
|
||||
**Depth**
|
||||
Leverage at the interface — the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface. A module is **shallow** when the interface is nearly as complex as the implementation.
|
||||
|
||||
**Seam** _(from Michael Feathers)_
|
||||
A place where you can alter behaviour without editing in that place. The _location_ at which a module's interface lives. Choosing where to put the seam is its own design decision, distinct from what goes behind it.
|
||||
_Avoid_: boundary (this repo uses "boundary" specifically for ESLint workspace-tag rules — keep it for that meaning).
|
||||
|
||||
**Adapter**
|
||||
A concrete thing that satisfies an interface at a seam. Describes _role_ (what slot it fills), not substance (what's inside). In this repo every port typically has at least two adapters (real + mock); some have three (real + mock + recording).
|
||||
|
||||
**Leverage**
|
||||
What callers get from depth. More capability per unit of interface they have to learn. One implementation pays back across N call sites and M tests.
|
||||
|
||||
**Locality**
|
||||
What maintainers get from depth. Change, bugs, knowledge, and verification concentrate at one place rather than spreading across callers. Fix once, fixed everywhere.
|
||||
|
||||
## Principles
|
||||
|
||||
- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface.
|
||||
- **The deletion test.** Imagine deleting the module. If complexity vanishes, the module wasn't hiding anything (it was a pass-through). If complexity reappears across N callers, the module was earning its keep.
|
||||
- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test _past_ the interface, the module is probably the wrong shape.
|
||||
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it. In this repo, the typical justification is "one real adapter + one mock for tests" — that's two.
|
||||
- **The manifest is a structural seam.** A feature's `feature.manifest.ts` declares its use cases / events / jobs / channels / required cores / coverage bands. Refactors that move behaviour between features MUST move manifest entries too; the conformance gates enforce this.
|
||||
|
||||
## Relationships
|
||||
|
||||
- A **Module** has exactly one **Interface** (the surface it presents to callers and tests).
|
||||
- **Depth** is a property of a **Module**, measured against its **Interface**.
|
||||
- A **Seam** is where a **Module**'s **Interface** lives.
|
||||
- An **Adapter** sits at a **Seam** and satisfies the **Interface**.
|
||||
- **Depth** produces **Leverage** for callers and **Locality** for maintainers.
|
||||
|
||||
## Mapping to this repo's identifiers
|
||||
|
||||
Abstract → concrete translation table. When proposing a deepening, name things using the right column.
|
||||
|
||||
| Abstract term | Where it lands in this repo |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Module** | **Primarily a feature** (`packages/<name>/`) — that's the canonical refactor scope. Also: a use case (`*.use-case.ts`), controller (`*.controller.ts`), repository port + adapters (`*.repository.{interface,mock,}.ts`), service port + adapters (`*.service.{interface,mock,}.ts`), binder (`bind-production.ts` / `bind-dev-seed.ts`), manifest (`feature.manifest.ts`), or a core package (`packages/core-<name>/`) — when the refactor operates at those narrower scales. When in doubt, say "feature". |
|
||||
| **Interface** | The exported types from a module file: `IXUseCase = ReturnType<typeof xUseCase>`, `IXController`, the `<x>.repository.interface.ts` shape, the manifest's declared keys, the Zod input/output schemas, the DI symbol contract. |
|
||||
| **Implementation** | The factory body, the adapter class body, what the binder wires. |
|
||||
| **Seam** | `<x>.repository.interface.ts`, `<x>.service.interface.ts`, the DI symbol (`*_SYMBOLS.IXRepository`), the manifest entry, a `// <gen:*>` anchor, the protocol types in `core-shared/di/bind-protocols.ts`. |
|
||||
| **Adapter** | `<x>.repository.ts` (Payload real) ↔ `<x>.repository.mock.ts` (in-memory). For instrumentation: `Noop*` ↔ `Otel*` ↔ `Recording*` (test). For bus: `InMemoryEventBus` ↔ `PayloadJobsEventBus`. |
|
||||
| **Test stand-in** | The `.mock.ts` adapter (constructed directly + injected into the factory). No container rebinding (ADR-012). |
|
||||
|
||||
## Rejected framings
|
||||
|
||||
- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout's original metric): rewards padding the implementation. We use depth-as-leverage instead.
|
||||
- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow — interface here includes every fact a caller must know, including manifest entries and DI symbols.
|
||||
- **"Boundary"** as a synonym for **seam**: this repo uses "boundary" specifically for ESLint workspace-tag rules (`feature` may depend on `core` + `tooling` only). Keep that meaning intact; say **seam** or **interface** when discussing features.
|
||||
- **"Service" as a generic term**: in this repo, **service** = a DI-injected port for non-collection capabilities (`IAuthenticationService`, `IMailerService`). Not a generic stand-in for "feature" or "the module doing the work."
|
||||
- **"Module" as the canonical noun**: avoid in everyday discourse — say **feature** (or **use case** / **controller** / **package** when narrower). "Module" is the abstract refactor vocabulary's word for the same thing, useful only when the abstraction across scales is the point.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- [`docs/glossary.md`](../../../docs/glossary.md) — project domain vocabulary (use case, manifest, slice, brand, coverage band, etc.)
|
||||
- [`docs/decisions/`](../../../docs/decisions/) — 21 ADRs that constrain the design space
|
||||
- [SKILL.md](SKILL.md) — the skill's process + hard constraints
|
||||
- [DEEPENING.md](DEEPENING.md) — dependency categories + seam discipline
|
||||
- [INTERFACE-DESIGN.md](INTERFACE-DESIGN.md) — parallel sub-agent design exploration
|
||||
104
.claude/skills/improve-codebase-architecture/SKILL.md
Normal file
104
.claude/skills/improve-codebase-architecture/SKILL.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
name: improve-codebase-architecture
|
||||
description: Find deepening opportunities in this repo, informed by docs/glossary.md and the ADRs in docs/decisions/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make the codebase more testable and AI-navigable. Respects the conformance system, boundary rules, and the 21 ADRs that govern shape.
|
||||
---
|
||||
|
||||
# Improve Codebase Architecture
|
||||
|
||||
Surface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability, scoped to what this template's existing rules permit.
|
||||
|
||||
## Glossary
|
||||
|
||||
Use these terms exactly in every suggestion. Consistent language is the point — don't drift into "component," "service" (we use that for DI ports specifically), or "boundary" (we use that for ESLint workspace-tag rules). Full definitions in [LANGUAGE.md](LANGUAGE.md).
|
||||
|
||||
- **Module** — **in this repo defaults to "feature"** (`packages/<name>/`). The abstract definition (anything with interface + implementation) still applies at narrower scales — a use case, controller, repository/service port, binder, or core package can also be a module — but say "feature" whenever that's the scope. Reach for "module" only when comparing depth/leverage across scales.
|
||||
- **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, schemas, DI shape. Not just the TypeScript type signature.
|
||||
- **Implementation** — the code inside.
|
||||
- **Depth** — leverage at the interface: a lot of behaviour behind a small interface. **Deep** = high leverage. **Shallow** = interface nearly as complex as the implementation.
|
||||
- **Seam** — where an interface lives; a place behaviour can be altered without editing in place. In this repo seams take a concrete shape: `*.interface.ts` files, DI symbols, manifest declarations, and `<gen:*>` anchors.
|
||||
- **Adapter** — a concrete thing satisfying an interface at a seam. In this repo: `<x>.repository.ts` (Payload real impl) vs `<x>.repository.mock.ts` vs `Recording*` test doubles.
|
||||
- **Leverage** — what callers get from depth.
|
||||
- **Locality** — what maintainers get from depth: change, bugs, knowledge concentrated in one place.
|
||||
|
||||
Key principles (see [LANGUAGE.md](LANGUAGE.md) for the full list):
|
||||
|
||||
- **Deletion test**: imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.
|
||||
- **The interface is the test surface.**
|
||||
- **One adapter = hypothetical seam. Two adapters = real seam.**
|
||||
|
||||
This skill is **informed by** the project's domain model and architecture decisions. Read [`docs/glossary.md`](../../../docs/glossary.md) for project vocabulary and the relevant ADR(s) in [`docs/decisions/`](../../../docs/decisions/) before proposing anything in their territory.
|
||||
|
||||
## Hard constraints (do not propose violations)
|
||||
|
||||
These are settled decisions — propose deepening WITHIN them, never against them:
|
||||
|
||||
- **Factory-function use cases & controllers** (ADR-012, ADR-013) — every use case is `(deps) => async (input) => output`; every controller is one verb-noun pair per file with a co-located `presenter`.
|
||||
- **Schemas in the use-case file** (ADR-013) — `xInputSchema`/`xOutputSchema` colocate with the factory; don't propose moving them to a separate module.
|
||||
- **Per-feature DI containers** (ADR-008) — don't propose a single global container.
|
||||
- **Five boundary tags + the dependency-direction matrix** (ADR-006, ADR-010) — features may depend only on `core` + `tooling`; cross-feature reactions go through `IEventBus` (ADR-015).
|
||||
- **Manifest-first ordering** (ADR-012, ADR-020) — new use cases land manifest → contracts → tests → impl; don't propose collapsing the steps.
|
||||
- **Brand-based conformance** — `Instrumented` / `Captured` / `Audited` are attached at DI bind time via `withSpan` / `withCapture` / `withAudit`; don't propose moving the wrapping elsewhere.
|
||||
- **Generator-first** — `pnpm turbo gen <kind>` is the entry point for new features/events/jobs/realtime channels. Don't propose hand-rolled scaffolding.
|
||||
- **Conventional Commits** (CLAUDE.md Key Conventions) — any refactor lands as conventional-commit messages.
|
||||
- **Hybrid versioning** (ADR-021) — refactors that move code between feature packages have version + CHANGELOG implications.
|
||||
|
||||
If a proposed deepening **would** violate an ADR, surface it explicitly with the ADR number and a "worth reopening because…" justification — but only if the friction is real enough. Most should be silently scoped out.
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Explore
|
||||
|
||||
Read [`docs/glossary.md`](../../../docs/glossary.md) and any ADRs in the area you're touching first. Then walk the codebase noting friction. The primary unit of attention is the **feature** (`packages/<name>/`); narrower units (use cases, controllers, repositories) get attention when the friction lives at that scale.
|
||||
|
||||
- Where does understanding one concept require bouncing between many small files across `entities/`, `application/`, `infrastructure/`, `interface-adapters/` inside a single feature?
|
||||
- Where is a feature **shallow** — its public surface (the `.` + `./ui` + `./api` exports) nearly as complex as its internal implementation? Or where inside a feature is a smaller unit shallow:
|
||||
- A `service.interface.ts` with one method that wraps a single repository call.
|
||||
- A `presenter` that's just `(x) => x`.
|
||||
- A controller body that's just `useCase(parsed.data)` with no transformation.
|
||||
- A repository wrapping another repository.
|
||||
- A use case wrapping another use case.
|
||||
- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)?
|
||||
- Where do tightly-coupled features leak across their seams? (e.g. a feature reaches into another feature's internals via deep import — though ESLint should catch this.)
|
||||
- Which parts are untested, or hard to test through their current interface? `pnpm coverage:diff` and `pnpm fallow` surface candidates.
|
||||
|
||||
Useful exploration shortcuts in this repo:
|
||||
|
||||
- `pnpm fallow` — dead exports, dupes, complexity hotspots, circular deps
|
||||
- `pnpm fallow:audit` — the AI-change audit; surfaces drift across recent edits
|
||||
- `git log --oneline --follow -- <path>` — change frequency is a depth signal
|
||||
- `cat packages/<feature>/src/feature.manifest.ts` — declared surface of a feature
|
||||
- `pnpm turbo boundaries` — workspace dependency graph
|
||||
|
||||
Apply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A "yes, concentrates" is the signal you want.
|
||||
|
||||
### 2. Present candidates
|
||||
|
||||
Present a numbered list of deepening opportunities. For each candidate:
|
||||
|
||||
- **Files** — which files / features / smaller units are involved (give exact paths)
|
||||
- **Problem** — why the current architecture is causing friction
|
||||
- **Solution** — plain English description of what would change
|
||||
- **Benefits** — explained in terms of **locality** and **leverage**, plus how tests would improve
|
||||
- **ADR impact** — any ADR this touches. If the proposed change conflicts with a current ADR, mark it explicitly: _"contradicts ADR-NNN — worth reopening because…"_ (only when the friction warrants it).
|
||||
- **Manifest impact** — if the change moves use cases / events / jobs / channels across features, the `feature.manifest.ts` of each feature involved will need an update; flag this so the user knows the conformance gates will require manifest edits before code edits (manifest-first ordering).
|
||||
|
||||
**Use [`docs/glossary.md`](../../../docs/glossary.md) vocabulary for the domain (use case, manifest, slice, feature, etc.) and [LANGUAGE.md](LANGUAGE.md) vocabulary for the architecture (module, seam, adapter, depth, leverage, locality).**
|
||||
|
||||
Do NOT propose interfaces yet. Ask the user: "Which of these would you like to explore?"
|
||||
|
||||
### 3. Grilling loop
|
||||
|
||||
Once the user picks a candidate, drop into a grilling conversation. Walk the design tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive.
|
||||
|
||||
Side effects happen inline as decisions crystallize:
|
||||
|
||||
- **Naming a deepened module after a concept not in [`docs/glossary.md`](../../../docs/glossary.md)?** Add the term to the glossary right there — same discipline as the `grill-with-docs` skill. Pick the appropriate section (Packages / Architecture layers / Feature building blocks / Conformance / Cross-feature / Instrumentation / Workflow / Releasing).
|
||||
- **Sharpening a fuzzy term during the conversation?** Update the glossary inline.
|
||||
- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as ADR-NNN so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future agent to avoid re-suggesting the same thing. The next ADR number is `001 + max(existing)` (currently `ADR-022`). Our ADR shape: `Context → Decision → Alternatives considered → Consequences → Related`.
|
||||
- **Refactor will move code between feature packages?** Flag the release-please impact: both affected packages will bump versions on the next release PR.
|
||||
- **Want to explore alternative interfaces for the deepened module?** See [INTERFACE-DESIGN.md](INTERFACE-DESIGN.md).
|
||||
|
||||
## Related skills
|
||||
|
||||
- `grill-with-docs` (`.claude/skills/grill-with-docs/`) — stress-tests plans against ADRs + glossary + manifests; share the same glossary-update discipline.
|
||||
- `to-prd` — if the deepening is large enough to merit a multi-task epic, materialize it as a PRD.
|
||||
108
.claude/skills/to-prd/SKILL.md
Normal file
108
.claude/skills/to-prd/SKILL.md
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
name: to-prd
|
||||
description: Turn the current conversation context into a PRD and write it to docs/work/prds/. Use when the user wants to materialize the discussion into a draft PRD that feeds the pnpm work pipeline.
|
||||
---
|
||||
|
||||
This skill takes the current conversation context and codebase understanding and produces a PRD. Do NOT interview the user — just synthesize what you already know. If you need to interview first, invoke `grill-with-docs` instead.
|
||||
|
||||
The PRD lives on the filesystem (this repo does not use an issue tracker for work). The downstream pipeline is `pnpm work decompose` → epic + stories → tasks → sandcastle dispatch (see `docs/architecture/agent-first-workflow-and-conformance.md`).
|
||||
|
||||
## Process
|
||||
|
||||
1. **Explore the repo if you haven't already.** Use the project's domain vocabulary throughout (check `docs/glossary.md` if it exists, otherwise lift terms from `docs/architecture/vertical-feature-spec.md` §6 and the feature packages' `feature.manifest.ts`). Respect any ADRs in the area you're touching — they're at `docs/decisions/adr-NNN-<slug>.md`. Use `pnpm work status` to see in-flight epics.
|
||||
|
||||
2. **Sketch the major modules / packages.** Identify which existing packages (`packages/<feature>/`, `packages/core-*/`) you'll modify and which new ones — if any — you'll create. Actively look for **deep modules**: small interface, deep implementation, rarely-changing surface. The vertical-feature-package shape (entities → application → infrastructure → DI) is the default unit; resist scaffolding new core packages unless required.
|
||||
|
||||
Check with the user that this module sketch matches their expectations. Confirm which modules they want tests written for. (The conformance system already mandates tests for every use case + controller; this question is about extra coverage — repository contract suites, integration tests, etc.)
|
||||
|
||||
3. **Pick a slug** for the PRD filename: `docs/work/prds/<kebab-slug>.prd.md`. No date prefix in the slug — the `created:` timestamp in frontmatter carries the date. Future task-tracker IDs (e.g. ClickUp) will land as `<task-id>-<kebab-slug>` once that integration ships; until then, bare slug only.
|
||||
|
||||
4. **Write the PRD using the template below**, then save it. Status starts at `draft`. The decomposer (`pnpm work decompose`) refuses to run on `draft` PRDs — the human flips it to `approved` after review.
|
||||
|
||||
<prd-template>
|
||||
|
||||
```markdown
|
||||
---
|
||||
id: <kebab-slug>
|
||||
title: <Human-readable title>
|
||||
type: prd
|
||||
status: draft
|
||||
author: <user>
|
||||
elicitation-session: <agent-session-id-or-omit>
|
||||
created: <ISO-8601-UTC-timestamp, e.g. 2026-05-14T19:23:45Z>
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
What's broken or missing today? Who hurts because of it? Frame it from the user's perspective (where "user" may be a developer using the template, an end-user of an app built on it, or an AI agent operating in the codebase).
|
||||
|
||||
## Goal
|
||||
|
||||
What state are we trying to reach? One or two sentences.
|
||||
|
||||
## In scope
|
||||
|
||||
- Bullets of what this PRD covers.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Bullets of what's explicitly excluded. The explicit no-s are as valuable as the yes-s.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Non-negotiables: existing ADRs to respect, conformance rules, performance budgets, compliance requirements, etc.
|
||||
- Reference ADRs by ID: `ADR-014`, `ADR-017`, etc.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Verifiable outcomes. "Feature X passes `pnpm typecheck && pnpm test && pnpm conformance` green" is concrete; "feature X is great" is not.
|
||||
|
||||
## User stories
|
||||
|
||||
A numbered list. Cover all aspects of the feature, including edge cases.
|
||||
|
||||
1. As a `<actor>`, I want `<capability>`, so that `<benefit>`.
|
||||
2. ...
|
||||
|
||||
## Implementation decisions
|
||||
|
||||
Decisions captured here so the decomposer (and downstream agents) don't re-litigate them. Include:
|
||||
|
||||
- Modules to be built / modified (by package or feature name — no file paths; those rot fast)
|
||||
- Interface shapes (Zod schemas, TypeScript types, tRPC procedures) — describe in prose; inline only if a snippet encodes the decision more precisely than prose (e.g., a Zod schema, a discriminated union, a state machine)
|
||||
- Architectural choices (DI factory shape, withSpan/withCapture wrapping, manifest entries, anchor placements)
|
||||
- Schema changes (Payload collections, database migrations)
|
||||
- Cross-feature interactions (event publish/consume pairs, realtime channels, audit emissions)
|
||||
- Optional-core requirements (does this feature require `core-events`? `core-realtime`? `core-audit`?)
|
||||
|
||||
Do NOT include specific file paths or full code snippets — they go stale quickly. Prefer prose plus inline contracts (schemas, types) where they tighten the decision.
|
||||
|
||||
## Testing decisions
|
||||
|
||||
- What "good test" means for this feature (behavior through public interfaces, not implementation details)
|
||||
- Which modules get repository contract suites (any new `IXRepository`)
|
||||
- Which modules get use-case unit tests (every use case — that's a conformance rule)
|
||||
- Integration / e2e coverage: which apps, which Playwright specs
|
||||
- Prior art in the codebase: pointers to similar test patterns to mirror
|
||||
|
||||
## Open questions
|
||||
|
||||
- Q1: `<question>` — `<recommended answer>`
|
||||
- Q2: ...
|
||||
|
||||
## Out of scope (deferred)
|
||||
|
||||
Things that are tempting to include but should be a separate PRD.
|
||||
|
||||
## Further notes
|
||||
|
||||
Anything else: stakeholders, related PRDs (`Builds on <prd-id>`, `Supersedes <prd-id>`), external references.
|
||||
```
|
||||
|
||||
</prd-template>
|
||||
|
||||
## After writing
|
||||
|
||||
- Verify the file lives at `docs/work/prds/<slug>.prd.md`.
|
||||
- Tell the user the path and remind them to review and flip `status: draft → approved` before running `pnpm work decompose`.
|
||||
- If new domain terms were introduced or sharpened during synthesis, append them to `docs/glossary.md` (lazy-create if missing) — same rules as `grill-with-docs`.
|
||||
50
.claude/skills/work-decompose/SKILL.md
Normal file
50
.claude/skills/work-decompose/SKILL.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: work-decompose
|
||||
description: Use when an approved PRD must be broken into an epic with story and task files under docs/work/. Triggers — the user asks to decompose a PRD, invokes /work-decompose, or wants a PRD turned into the work tree.
|
||||
---
|
||||
|
||||
# work-decompose
|
||||
|
||||
Decompose an `approved` PRD into an epic + story files under `docs/work/epics/`, by dispatching a **decomposer sub-agent** whose role is defined by the existing Sandcastle prompt.
|
||||
|
||||
This is the in-session, skill form of `pnpm work decompose --execute`. It adds a path; it changes nothing about `.sandcastle/` or `pnpm work`.
|
||||
|
||||
## Single source of truth — do not copy the prompt
|
||||
|
||||
The decomposer's role is defined in **`.sandcastle/decomposer.prompt.md`** — the same file `pnpm work decompose --execute` consumes. This skill **reads that file at dispatch time and passes it verbatim**. Never paraphrase, summarise, or inline it here. If this skill and the prompt ever disagree, **the prompt wins** — fix this skill, not the prompt.
|
||||
|
||||
## Process
|
||||
|
||||
1. **Resolve the PRD.** The user names a PRD (slug or path); otherwise list `docs/work/prds/*.prd.md` and ask which. Read the file.
|
||||
|
||||
2. **Refuse drafts.** If the PRD's frontmatter `status:` is not `approved`, **stop** — tell the user to flip it after review. The decomposer refuses drafts; catch it early.
|
||||
|
||||
3. **Build the prompt.** Read `.sandcastle/decomposer.prompt.md`. Substitute its `{{PRD_FILE_CONTENT}}` placeholder with the full PRD file contents.
|
||||
|
||||
4. **Dispatch the decomposer sub-agent.** Use the Agent tool, `general-purpose`. Its instructions are the substituted prompt, followed by this environment-adaptation note (the note adapts the environment — it is not a prompt edit):
|
||||
|
||||
> **Environment:** you are a Claude Code sub-agent, not running inside Sandcastle. Ignore the `<promise>COMPLETE</promise>` marker instruction — there is no iteration loop; just return your final summary. Write the epic + story files to `docs/work/epics/`. **Do not commit** — leave the files for the human to review and commit, per your own "offer them a chance to review + edit" step.
|
||||
|
||||
5. **Report.** Relay the epic folder path the sub-agent created. Remind the user to review/edit the stories, then commit, and that `pnpm work rebuild-state` (or the pre-commit hook) refreshes `_state.json`. The next step in the pipeline is `/work-dispatch`.
|
||||
|
||||
## Why a sub-agent
|
||||
|
||||
Decomposition is a self-contained, read-heavy job — the whole PRD, the slice-rule reasoning, the file-writing. Running it in a sub-agent keeps all of that out of the main session; you get back only the epic path.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| | |
|
||||
| ----------------- | ------------------------------------------------------------------ |
|
||||
| Input | an `approved` PRD in `docs/work/prds/` |
|
||||
| Role prompt | `.sandcastle/decomposer.prompt.md` — read, never copied |
|
||||
| Sub-agent | one `general-purpose` agent |
|
||||
| Output | `docs/work/epics/<epic-id>/` — `_epic.md` + `NN-<story>/_story.md` |
|
||||
| Upstream | `to-prd` / `grill-with-docs` produce the PRD |
|
||||
| Downstream | `/work-dispatch` runs the tasks |
|
||||
| Sandcastle parity | mirrors `pnpm work decompose --execute` |
|
||||
|
||||
## Common mistakes
|
||||
|
||||
- **Copying the prompt into this skill.** `.sandcastle/decomposer.prompt.md` is the source of truth — read it at dispatch time, every time.
|
||||
- **Decomposing a `draft` PRD.** Check `status: approved` first.
|
||||
- **Letting the sub-agent commit.** It writes files; the human reviews and commits.
|
||||
76
.claude/skills/work-dispatch/SKILL.md
Normal file
76
.claude/skills/work-dispatch/SKILL.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: work-dispatch
|
||||
description: Use when a task in the docs/work/ tree should be implemented and reviewed. Triggers — the user asks to dispatch a task, run the next task, run the implement-review loop, or invokes /work-dispatch.
|
||||
---
|
||||
|
||||
# work-dispatch
|
||||
|
||||
Run one work-tree task through the **implement → review loop**, using two separate sub-agents whose roles are defined by the existing Sandcastle prompts.
|
||||
|
||||
This is the in-session, skill form of `pnpm work dispatch --execute`. It adds a path; it changes nothing about `.sandcastle/` or `pnpm work`.
|
||||
|
||||
## Single source of truth — do not copy the prompts
|
||||
|
||||
Two role definitions, two files, read at dispatch time — **never copied**:
|
||||
|
||||
- implementer role → **`.sandcastle/implementer.prompt.md`**
|
||||
- reviewer role → **`.sandcastle/reviewer.prompt.md`**
|
||||
|
||||
`pnpm work dispatch --execute` (Sandcastle) and this skill consume the **same** files. Never paraphrase or inline them. If a prompt and this skill disagree, **the prompt wins**. This skill is only the _wiring_: pick the task, substitute placeholders, dispatch the sub-agents, run the loop.
|
||||
|
||||
## Separate sub-agents — non-negotiable
|
||||
|
||||
The implementer and the reviewer are **distinct sub-agents**, dispatched separately:
|
||||
|
||||
- The **implementer** writes code. Dispatch a `general-purpose` Agent with `isolation: "worktree"` — it works on an isolated git worktree + branch, so bad output never touches your main tree.
|
||||
- The **reviewer** must be a _different_ agent and must _not_ write. Dispatch an `Explore` Agent (read-only by construction) — it cannot Edit/Write the repo even by accident; it only verifies the diff.
|
||||
|
||||
Never collapse the two into one agent. An agent that writes code and then grades its own work has not been reviewed.
|
||||
|
||||
## Process
|
||||
|
||||
1. **Resolve the task.** Default: run `pnpm work next`, then read the first unchecked task file of that story (`docs/work/epics/<epic>/<story>/NN-<slug>.task.md`). Or the user names a task id. Read the task file; note its `max-attempts` frontmatter (default 3).
|
||||
|
||||
2. **Dispatch the implementer.** Read `.sandcastle/implementer.prompt.md`, substitute `{{TASK_FILE_CONTENT}}` with the task file. Dispatch a `general-purpose` Agent with `isolation: "worktree"`; instructions = the substituted prompt + this environment-adaptation note:
|
||||
|
||||
> **Environment:** a Claude Code sub-agent in a fresh, isolated git worktree — NOT a Sandcastle Docker sandbox. Run `pnpm install` as your first step (the worktree has no `node_modules`). Commit your slice on a branch. **Report that branch name** in the `notes` field of your output JSON. Ignore the `<promise>COMPLETE</promise>` marker — there is no iteration loop; just return the structured JSON as your final message.
|
||||
|
||||
Read the returned JSON: `status`, `commit_sha`, `files_changed`, `notes` (which carries the branch name).
|
||||
|
||||
3. **Handle the implementer's status.** `complete` → step 4. `blocked` or `needs-clarification` → surface the `notes` to the user and stop; do not proceed to review.
|
||||
|
||||
4. **Compute the diff.** `git diff main...<task-branch>` from the main tree — git worktrees share `.git`, so the implementer's branch is visible.
|
||||
|
||||
5. **Dispatch the reviewer.** Read `.sandcastle/reviewer.prompt.md`, substitute `{{TASK_FILE_CONTENT}}` and `{{DIFF}}`. Dispatch an `Explore` Agent (read-only); instructions = the substituted prompt + this environment-adaptation note:
|
||||
|
||||
> **Environment:** a Claude Code sub-agent reviewing a LOCAL branch — there is no pull request or CI run yet. Where the prompt says to trust Sandcastle's CI step: the implementer has already run and reported the five conformance gates + coverage as its commit precondition — verify AC coverage, out-of-scope discipline, and slice discipline by **reading the diff**, as your checks describe. Run the library-trace check directly (`node scripts/library-decisions/check.mjs`). The Socket and CodeQL steps require a CI run — note them as "deferred to CI", do not block on them. Ignore the `<promise>COMPLETE</promise>` marker; return the JSON decision.
|
||||
|
||||
Read the returned JSON: `decision`, `scope_violations`, `notes`.
|
||||
|
||||
6. **Run the loop.**
|
||||
- **`approve`** → merge `<task-branch>` into `main`; remove the worktree. Then run `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance` once on `main` as a post-merge safety check. Print the suggested task-checkbox / `_state.json` mutation for the user to apply — **do not write state yourself.**
|
||||
- **`reject`** → re-dispatch the implementer (step 2) with the reviewer's `notes` appended to its instructions. Repeat until `approve`, or until the task's `max-attempts` is reached — then stop and surface the last reviewer notes.
|
||||
|
||||
## Why this shape
|
||||
|
||||
State mutation stays manual (the skill suggests, the human applies) — exactly as Sandcastle's v1 orchestrator does, and consistent with the implementer prompt's "the orchestrator handles state writes." Worktree isolation gives the implementer a clean room without Docker. The read-only reviewer makes "the reviewer does not modify the repo" a property of the tool, not a promise.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| | |
|
||||
| ----------------- | ------------------------------------------------------------------------------------- |
|
||||
| Implementer | `.sandcastle/implementer.prompt.md` · `general-purpose` · `isolation: "worktree"` |
|
||||
| Reviewer | `.sandcastle/reviewer.prompt.md` · `Explore` (read-only) |
|
||||
| Placeholders | implementer: `{{TASK_FILE_CONTENT}}` · reviewer: `{{TASK_FILE_CONTENT}}` + `{{DIFF}}` |
|
||||
| Loop cap | the task's `max-attempts` frontmatter (default 3) |
|
||||
| State writes | suggested to the user, never applied by the skill |
|
||||
| Upstream | `/work-decompose` produces the tasks |
|
||||
| Sandcastle parity | mirrors `pnpm work dispatch --execute` |
|
||||
|
||||
## Common mistakes
|
||||
|
||||
- **Copying a prompt into this skill.** The `.sandcastle/*.prompt.md` files are the source of truth — read them at dispatch time.
|
||||
- **One agent for both roles.** Implementer and reviewer are separate sub-agents; the reviewer is `Explore` (read-only).
|
||||
- **Writing `_state.json` or ticking the checkbox.** The skill prints the suggested mutation; the human applies it.
|
||||
- **Skipping the environment-adaptation note.** Without it the sub-agent follows Sandcastle-only instructions — the `<promise>` marker, "trust CI" — that do not apply in-session.
|
||||
- **Reviewing before the implementer says `complete`.** A `blocked` status stops the loop; do not review a partial slice.
|
||||
Reference in New Issue
Block a user