@data-matters.io/agent-harness 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +34 -2
  2. package/bin/harness +338 -0
  3. package/package.json +28 -3
  4. package/scripts/next-agent-rules.sh +67 -0
  5. package/scripts/toolchain.sh +14 -0
  6. package/standards/conventions.md +68 -0
  7. package/standards/dod.md +53 -0
  8. package/standards/stack.md +42 -0
  9. package/standards/stacks/nestjs/architecture.md +75 -0
  10. package/standards/stacks/nestjs/conventions.md +33 -0
  11. package/standards/stacks/nestjs/stack.md +34 -0
  12. package/standards/stacks/nestjs/testing.md +31 -0
  13. package/standards/stacks/nextjs/architecture.md +97 -0
  14. package/standards/stacks/nextjs/conventions.md +32 -0
  15. package/standards/stacks/nextjs/stack.md +40 -0
  16. package/standards/stacks/nextjs/testing.md +33 -0
  17. package/standards/testing.md +58 -0
  18. package/templates/.claude/hooks/stop-gates.sh +43 -0
  19. package/templates/.claude/settings.json +72 -0
  20. package/templates/.github/workflows/gates.yml +41 -0
  21. package/templates/.harness/db-smoke.sh +75 -0
  22. package/templates/.harness/deps.sh +219 -0
  23. package/templates/.harness/gates.conf.example +26 -0
  24. package/templates/.harness/gates.sh +108 -0
  25. package/templates/.harness/lib.sh +129 -0
  26. package/templates/.harness/loop.sh +101 -0
  27. package/templates/.harness/model-flags.sh +34 -0
  28. package/templates/.harness/models.conf +30 -0
  29. package/templates/.harness/notify.sh +49 -0
  30. package/templates/.harness/openspec.sh +28 -0
  31. package/templates/.harness/pr-scope.sh +44 -0
  32. package/templates/.harness/run-tasks.sh +422 -0
  33. package/templates/.harness/spec-pr.sh +103 -0
  34. package/templates/.harness/status.sh +155 -0
  35. package/templates/CLAUDE.md.tmpl +137 -0
  36. package/templates/openspec/config.yaml +38 -0
  37. package/templates/stacks/nestjs/gates.conf +32 -0
  38. package/templates/stacks/nestjs/gates.yml +71 -0
  39. package/templates/stacks/nextjs/claude-imports.md +4 -0
  40. package/templates/stacks/nextjs/gates.conf +33 -0
  41. package/templates/stacks/nextjs/gates.yml +68 -0
@@ -0,0 +1,75 @@
1
+ # Architecture: NestJS
2
+
3
+ Architecture for backend services, as established by the app template
4
+ (`nestjs-new-app`). Deviations need an ADR in the product repo.
5
+
6
+ ## Layout
7
+
8
+ ```
9
+ src/
10
+ ├── main.ts, app.module.ts # composition root
11
+ ├── modules/<feature>/ # self-contained feature modules
12
+ │ ├── domain/ # entities, value objects, errors, ports — no framework imports
13
+ │ ├── application/ # services orchestrating the domain (use cases)
14
+ │ ├── adapters/
15
+ │ │ ├── inbound/ # controllers
16
+ │ │ └── outbound/ # Prisma repositories implementing domain ports
17
+ │ ├── dto/ # request/response shapes, validated with Zod
18
+ │ ├── <feature>.module.ts
19
+ │ └── README.md
20
+ └── shared/ # cross-cutting infrastructure (config, database,
21
+ # problem-details, authn, authz, observability, …)
22
+ db/ # schema.prisma, migrations/ (local), seed.ts
23
+ deploy/ # k8s manifests, reviewed SQL for deployed databases
24
+ test/ # e2e specs
25
+ ```
26
+
27
+ `me` is the reference for a feature module at its smallest; `health` shows the
28
+ same shape with an extension seam (indicators) instead of a database.
29
+
30
+ ## Dependency rules
31
+
32
+ | Rule | Enforced by |
33
+ | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
34
+ | A feature module never imports another feature module; shared code lives in `@/shared/*` | `@data-matters.io/nestjs-module-boundaries` (lint, error) |
35
+ | No parent-relative imports: use `@/` or a same-directory `./` | `@data-matters.io/no-parent-relative-imports` (lint, error) |
36
+ | No `process.env` outside the config layer (config loader and secrets bootstrap are exempt) | `@data-matters.io/no-process-env` (lint, error) |
37
+ | `domain/` has no framework or Prisma imports | architecture test (`*.arch.spec.ts`) |
38
+ | Controllers depend on application services, services on domain ports; adapters implement ports | PR review |
39
+
40
+ ## Request flow
41
+
42
+ ```
43
+ controller (adapters/inbound) → Zod-validated DTO, @RequireAccess
44
+ → application service → domain logic, depends on ports only
45
+ → repository port (domain) ← Prisma adapter (adapters/outbound)
46
+ ```
47
+
48
+ - **Validation** at the boundary with the Zod validation pipe; DTO types come
49
+ from the schemas.
50
+ - **Errors** are RFC 9457 problem details. Throw subclasses of the abstract
51
+ `AppProblemError`; the global `problem-details` filter renders them. Never
52
+ hand-build error bodies.
53
+ - **Configuration** only through the typed `ConfigService`; new settings get a
54
+ Zod schema in their module's `schema/` and fail fast at startup.
55
+ - **Persistence** follows `src/shared/database/README.md`: one pooled
56
+ `PrismaService`, repositories behind domain ports, explicit mappers,
57
+ interactive transactions for multi-write operations.
58
+ - **Schema changes**: `db/schema.prisma` is the source of truth. Local
59
+ migrations via `make db-migrate`; deployed databases receive a reviewed SQL
60
+ diff (`make db-diff`) in `deploy/db/`, applied by the data pipeline. The app
61
+ never migrates on startup or deploy, so changes must be expand/contract
62
+ compatible.
63
+
64
+ ## Authentication and authorization
65
+
66
+ Entra ID authentication is optional and switchable by configuration. Every
67
+ route is authenticated unless marked `@Public()`; protected routes declare
68
+ their access level with `@RequireAccess`, and tests cover both the allowed and
69
+ the denied path.
70
+
71
+ ## Observability
72
+
73
+ Log through the injected Pino logger (structured, with correlation IDs and
74
+ redaction), never `console`. Outbound HTTP calls use the shared headers module
75
+ so correlation and trace context propagate.
@@ -0,0 +1,33 @@
1
+ # Conventions: NestJS
2
+
3
+ Extends the shared `conventions.md`.
4
+
5
+ ## Code
6
+
7
+ - Aliases: `@/…` (maps to `src/`) and `@root/…` (repo root). No `../`.
8
+ - Classes use NestJS decorators; dependency injection through constructors.
9
+ Ports are injected by token, not by concrete class.
10
+ - Each feature module and each shared module has a `README.md` describing its
11
+ purpose, configuration and extension points.
12
+
13
+ ## Naming
14
+
15
+ | Kind | Convention | Example |
16
+ | ----------------------------- | ------------------------------------------------------------------------- | ------------------------------- |
17
+ | Feature module folder | kebab-case under `src/modules/` | `src/modules/me/` |
18
+ | Module / controller / service | `<name>.module.ts`, `<name>.controller.ts`, `<name>.service.ts` | `health.controller.ts` |
19
+ | DTO | `dto/<name>.dto.ts` | `dto/me-response.dto.ts` |
20
+ | Port | `domain/ports/<name>.port.ts` (use-case ports end in `.use-case.port.ts`) | `check-health.use-case.port.ts` |
21
+ | Config schema | `schema/<name>.schema.ts` | `schema/database.schema.ts` |
22
+ | Unit test | `<file>.spec.ts` beside the code or in `__tests__/` | `health.service.spec.ts` |
23
+ | Property-based test | `<topic>.pbt.spec.ts` | `headers.pbt.spec.ts` |
24
+ | Architecture test | `<topic>.arch.spec.ts` | `domain-purity.arch.spec.ts` |
25
+ | Integration test | `<topic>.integration.test.ts` | `prisma.integration.test.ts` |
26
+ | E2E test | `test/<topic>.e2e-spec.ts` | `test/auth.e2e-spec.ts` |
27
+ | Prisma models | PascalCase models, camelCase fields, UPPER_CASE enum values | — |
28
+ | Env vars | `UPPER_SNAKE_CASE`, grouped by prefix (`DB_…`, `AUTH_…`, `DD_…`) | `DB_POOL_MAX` |
29
+
30
+ ## Local workflow
31
+
32
+ `make setup`, `make dev`, `make ci-check`; database targets `make db-migrate`,
33
+ `make db-diff`, `make db-verify`.
@@ -0,0 +1,34 @@
1
+ # Stack: NestJS backend services
2
+
3
+ Template: `data-matters-io/nestjs-new-app` (reviewed at `b916be8`, with
4
+ `@data-matters.io/eslint-config` 4.0.0).
5
+
6
+ | Area | Choice | Version |
7
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
8
+ | Runtime | Node.js | 22.x+ |
9
+ | Framework | NestJS on Express (`@nestjs/platform-express`) | 11.x |
10
+ | Architecture | Hexagonal feature modules (see `architecture.md`) | — |
11
+ | Config | Typed, Zod-validated `ConfigService`, loaded once at startup (fail-fast) | — |
12
+ | Secrets | AWS Secrets Manager resolved into the config pipeline | — |
13
+ | API docs | OpenAPI via `@nestjs/swagger` (Zod-backed decorators) | 11.x |
14
+ | Errors | RFC 9457 problem details (`application/problem+json`) via a global filter | — |
15
+ | HTTP hardening | Helmet, CORS, `@nestjs/throttler` rate limiting | — |
16
+ | Auth | Microsoft Entra ID (JWT via `jose`, BFF cookie session via `@azure/msal-node`), optional | — |
17
+ | Authorization | `@RequireAccess` + `AccessLevelGuard` | — |
18
+ | Observability | Pino structured logs, correlation IDs, Datadog transport, OpenTelemetry API | — |
19
+ | Database migrations | `prisma migrate dev` locally only; deployed schemas change through a reviewed SQL diff (`prisma migrate diff --script`) run by the data pipeline, never by the app | — |
20
+ | Testing | Jest + `supertest` (e2e), `fast-check` (property-based), Testcontainers (integration) | 30.x |
21
+ | Lint preset | `nestjs` (typed linting, `@darraghor/nestjs-typed`, module-boundary rules) | 4.x |
22
+ | Packaging / deploy | Docker image; Kubernetes manifests in `deploy/k8s` | — |
23
+
24
+ ## Gates (`templates/stacks/nestjs/gates.conf`)
25
+
26
+ | Gate | Command |
27
+ | ------------------- | -------------------------------------------------------------------------------------------- |
28
+ | setup | `prisma generate` |
29
+ | test | `jest --coverage --coverageThreshold` (floor: 85% lines, statements, branches and functions) |
30
+ | lint | `eslint . --max-warnings 0` |
31
+ | typecheck | `tsc --noEmit -p tsconfig.json` (includes tests; `tsconfig.build.json` does not) |
32
+ | build | `nest build` |
33
+ | e2e (extra) | `jest --config ./test/jest-e2e.json` |
34
+ | integration (extra) | `jest --config ./test/jest-integration.json` (needs Docker or Podman) |
@@ -0,0 +1,31 @@
1
+ # Testing: NestJS
2
+
3
+ Extends the shared `testing.md`.
4
+
5
+ Tooling: Jest 30 + `ts-jest` (`testEnvironment: node`), `@nestjs/testing`,
6
+ `supertest` for HTTP, `fast-check` for property-based tests, Testcontainers
7
+ (`@testcontainers/postgresql`) for integration.
8
+
9
+ ## Tiers
10
+
11
+ | Tier | Pattern | Config | Status |
12
+ | -------------------------------------------------- | ------------------------------ | ---------------------------- | ------------------------------------------------------------ |
13
+ | Unit (incl. property-based and architecture tests) | `src/**/*.spec.ts` | `package.json#jest` | in use; `test` gate |
14
+ | Integration | `src/**/*.integration.test.ts` | `test/jest-integration.json` | in use; `integration` extra gate (Docker or Podman required) |
15
+ | E2E | `test/*.e2e-spec.ts` | `test/jest-e2e.json` | in use; `e2e` extra gate |
16
+
17
+ ## Rules
18
+
19
+ - Unit tests build the module under test with `Test.createTestingModule`,
20
+ replacing ports with fakes; no database, no network.
21
+ - Invariants that hold for any input (header maps, redaction, pagination,
22
+ transaction atomicity) get a property-based test.
23
+ - Repository adapters are covered by integration tests against a real
24
+ PostgreSQL started by Testcontainers, never by mocking Prisma.
25
+ - E2E tests boot the application and assert on HTTP status, headers and
26
+ RFC 9457 bodies, including the unauthenticated and forbidden paths.
27
+
28
+ ## Coverage floor
29
+
30
+ 85% on lines, statements, branches and functions over every file in `src/`
31
+ (template measured 92.0% / 92.7% / 90.8% / 93.5% on 2026-10-01).
@@ -0,0 +1,97 @@
1
+ # Architecture: Next.js
2
+
3
+ Architecture for Next.js products, as established by the app template
4
+ (`nextjs-new-app`). Deviations need an ADR in the product repo.
5
+
6
+ ## Feature-Based Architecture (FBA)
7
+
8
+ A feature is self-contained: deleting its folder must not break the project.
9
+
10
+ ```
11
+ app/
12
+ ├── (features)/ # route group; not part of the URL
13
+ │ ├── (<feature>)/ # one route group per feature
14
+ │ │ ├── <route>/page.tsx # pages
15
+ │ │ ├── api/<route>/route.ts # route handlers (thin)
16
+ │ │ ├── api/<route>/<entity>.controller.ts
17
+ │ │ ├── components/ # feature components (+ __tests__/)
18
+ │ │ ├── hooks/ # feature hooks (+ __tests__/)
19
+ │ │ ├── contexts/ # feature React contexts
20
+ │ │ ├── types/ # feature types
21
+ │ │ └── test-fixtures.ts # make<Entity>() factories for tests
22
+ │ ├── layout.tsx
23
+ │ └── page.tsx
24
+ ├── shared/ # code used by 3+ features
25
+ │ ├── components/{ui,layout,auth}/
26
+ │ ├── config/ # rbac.ts, route-permissions.ts, menuItems.json
27
+ │ ├── core/ # BaseController
28
+ │ ├── hooks/ libs/ providers/ types/ utils/ messages/
29
+ db/ # schema.prisma, migrations/, seed.ts
30
+ proxy.ts # Next.js middleware (route protection)
31
+ ```
32
+
33
+ ## Dependency rules
34
+
35
+ | Rule | Enforced by |
36
+ | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
37
+ | A feature never imports from another feature | `@data-matters.io/fba-boundaries` (lint, error) |
38
+ | `shared/` never imports from `(features)/` | `@data-matters.io/fba-boundaries` (lint, error) |
39
+ | Code moves to `shared/` only when a third feature needs it (rule of three) | PR review |
40
+ | Cross-folder imports use path aliases (`@/`, `@shared/*`, `@features/*`), not `../../` | `@data-matters.io/no-deep-relative-imports` (lint, warn → blocks with `--max-warnings 0`) |
41
+
42
+ ## Request flow (server)
43
+
44
+ ```
45
+ proxy.ts (authn, route permissions)
46
+ → route.ts thin: check permission, delegate, map result to NextResponse
47
+ → <Entity>Controller extends BaseController validation (Zod) + data access
48
+ → Prisma
49
+ ```
50
+
51
+ - **Route handlers** check permissions with `requirePermission(request, PERMISSIONS.X)`
52
+ first, then delegate to the controller. No business logic or Prisma calls in
53
+ `route.ts`.
54
+ - **Controllers** extend `app/shared/core/base-controller.ts` for CRUD
55
+ (pagination, search via `buildSearchFilter`, Prisma error mapping) and
56
+ override only what differs.
57
+ - **Validation** at the boundary with Zod schemas. Schema-first: prefer
58
+ deriving TypeScript types from schemas (`z.infer`) or Prisma over
59
+ hand-written duplicates.
60
+ - **Response contract**: build responses with `app/shared/libs/http-responses.ts`,
61
+ never hand-built `NextResponse.json(...)`. Collections are JSON:API documents
62
+ (`{ data, meta, links }`, `buildPaginatedResponse`); errors are RFC 9457
63
+ problem details (`application/problem+json`, `problemResponse` and its
64
+ status shortcuts, `validationProblem` for Zod errors).
65
+ - **Errors**: Prisma errors map to problem details (P2002 → 409, P2025 → 404,
66
+ P2003 → 400). Log details only in development; never leak internals in
67
+ production responses.
68
+ - Route handler params are `Promise`s in Next.js 16: `await params`.
69
+
70
+ ## Authorization (RBAC)
71
+
72
+ Three layers, all driven by `app/shared/config/rbac.ts` (`PERMISSIONS`) and
73
+ `route-permissions.ts`:
74
+
75
+ 1. **Middleware** (`proxy.ts`): blocks unauthorized routes.
76
+ 2. **API**: `requirePermission` in every route handler that mutates or reads
77
+ protected data.
78
+ 3. **UI**: `RequireAuth` / `RequirePermission` / `RequireRole` components and
79
+ `useUserAccess`; menu entries declare `permissions`/`roles`.
80
+
81
+ A new protected capability adds a permission constant, protects all three
82
+ layers and has tests for the denied path.
83
+
84
+ ## Client
85
+
86
+ - Data fetching in feature hooks (`useX`, `useCreateX`, …) returning
87
+ `{ data, isLoading, error, … }`; components stay presentational.
88
+ - UI primitives come from `@/app/shared/components/ui` (shadcn/ui). Do not
89
+ add another component library.
90
+ - Accessibility target: WCAG 2.1 AA (labels, keyboard navigation, focus,
91
+ contrast). jsx-a11y rules run in lint.
92
+
93
+ ## Configuration and secrets
94
+
95
+ - Configuration from environment variables; `env-example` documents every
96
+ variable with **placeholder values only**.
97
+ - `.env*` files are git-ignored and never read by agents.
@@ -0,0 +1,32 @@
1
+ # Conventions: Next.js
2
+
3
+ Extends the shared `conventions.md`.
4
+
5
+ ## Code
6
+
7
+ - Components are arrow functions (`react/function-component-definition`).
8
+ - Type-only imports inline: `import { type Foo } from '…'`.
9
+ - Import order (auto-fixed by lint): builtin → external → internal (`@/…`) →
10
+ parent → sibling → index → type, alphabetical, blank line between groups,
11
+ React first.
12
+ - Aliases: `@/*`, `@app/*`, `@shared/*`, `@features/*`.
13
+ - User-facing strings go through `app/shared/messages/{en,es,pt}.json`.
14
+
15
+ ## Naming
16
+
17
+ | Kind | Convention | Example |
18
+ | ------------------- | ----------------------------------------------------------- | ---------------------------------- |
19
+ | Feature folder | route group, kebab-case | `app/(features)/(admin)/` |
20
+ | Feature component | PascalCase `.tsx` | `UserForm.tsx` |
21
+ | shadcn/ui primitive | kebab-case `.tsx` (shadcn default) | `radio-group.tsx` |
22
+ | Hook | `useX.ts`, one hook per file | `useCreateUser.ts` |
23
+ | Controller | `<entity>.controller.ts` next to its route | `user.controller.ts` |
24
+ | Types | `types/<entity>.ts` | `types/user.ts` |
25
+ | Tests | `__tests__/<file>.test.ts(x)` beside the code | `hooks/__tests__/useUsers.test.ts` |
26
+ | Test factories | `make<Entity>()` in `test-fixtures.ts` | `makeUser()` |
27
+ | Prisma models | PascalCase models, camelCase fields, UPPER_CASE enum values | `User.status: ACTIVE` |
28
+ | Env vars | UPPER_SNAKE_CASE | `NEXTAUTH_SECRET` |
29
+
30
+ ## Local workflow
31
+
32
+ `make setup`, `make dev`, `make ci-check` (format → lint → typecheck → tests).
@@ -0,0 +1,40 @@
1
+ # Stack: Next.js web applications
2
+
3
+ Template: `data-matters-io/nextjs-new-app` (reviewed at `164057b`, with
4
+ `@data-matters.io/eslint-config` 4.0.0).
5
+
6
+ | Area | Choice | Version |
7
+ | -------------- | ------------------------------------------------------------------------------------------------------------- | ------- |
8
+ | Runtime | Node.js | 24.x |
9
+ | Framework | Next.js, App Router, Turbopack builds | 16.x |
10
+ | UI | React | 19.x |
11
+ | Styling | Tailwind CSS | 4.x |
12
+ | Components | shadcn/ui (Radix UI primitives), `lucide-react` icons, `class-variance-authority` + `tailwind-merge` (`cn()`) | — |
13
+ | Theming | `next-themes` (dark mode) | — |
14
+ | Auth | NextAuth.js (Google provider) + in-app RBAC | 4.x |
15
+ | API responses | JSON:API documents; errors as RFC 9457 problem details | — |
16
+ | Notifications | `react-hot-toast` | — |
17
+ | Error tracking | GlitchTip via `@sentry/nextjs` | — |
18
+ | i18n | In-app messages (`app/shared/messages/{en,es,pt}.json`) | — |
19
+ | Testing | Jest + React Testing Library (jsdom) | 30.x |
20
+ | Lint preset | `nextjs` (React, a11y, Core Web Vitals, FBA rules) | 4.x |
21
+
22
+ ## Agent rules from Next.js (ADR 0018)
23
+
24
+ `next dev` keeps a version-matched "agent rules" block in `AGENTS.md`, which is
25
+ committed and owned by Next.js. The harness-rendered `CLAUDE.md` imports it
26
+ (`@AGENTS.md`), and the block never goes into `CLAUDE.md`:
27
+ `scripts/next-agent-rules.sh` (run by `init-project.sh` and `sync-project.sh`)
28
+ moves it out of `CLAUDE.md` and creates `AGENTS.md` when it is missing. Commit
29
+ `AGENTS.md` changes with the Next.js upgrade that causes them.
30
+
31
+ ## Gates (`templates/stacks/nextjs/gates.conf`)
32
+
33
+ | Gate | Command |
34
+ | ---------------- | --------------------------------------------------------------------------------- |
35
+ | setup | `prisma generate` |
36
+ | test | `jest --coverageThreshold` (floor: 85% lines, statements, branches and functions) |
37
+ | lint | `eslint . --max-warnings 0` |
38
+ | typecheck | `tsc --noEmit` |
39
+ | build | `next build` |
40
+ | integration, e2e | pending (see `testing.md`) |
@@ -0,0 +1,33 @@
1
+ # Testing: Next.js
2
+
3
+ Extends the shared `testing.md`.
4
+
5
+ Tooling: Jest 30 + React Testing Library (jsdom), `ts-jest`, setup in
6
+ `jest.setup.tsx`, helpers in `app/shared/test-utils.tsx` and
7
+ `app/shared/test-helpers.ts`. The template also has an e2e smoke test
8
+ (`scripts/smoke-test.ts`, `pnpm test:smoke`) run by its own `ci.yaml` against
9
+ a built app and PostgreSQL.
10
+
11
+ ## Tiers
12
+
13
+ | Tier | Pattern | Status |
14
+ | ----------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- |
15
+ | Unit | `__tests__/*.test.ts(x)` beside the code | in use; `test` gate |
16
+ | Integration | `*.integration.test.ts` (controllers + Prisma against PostgreSQL via Testcontainers, as in `nestjs-new-app`) | **pending**: to be implemented in a future change |
17
+ | E2E | `pnpm test:smoke` | template CI only; not yet a harness gate |
18
+
19
+ ## Rules
20
+
21
+ - Unit tests do not touch the network or the database: mock `fetch` and the
22
+ Prisma client. Controllers are tested through their public methods with a
23
+ real `Request`.
24
+ - Components are tested through accessible queries (`getByRole`,
25
+ `getByLabelText`), not implementation details.
26
+ - RBAC: every protected route/component tests both the allowed and the denied
27
+ path.
28
+
29
+ ## Coverage floor
30
+
31
+ 85% on lines, statements, branches and functions, over every file in `app/`
32
+ plus `proxy.ts` (`collectCoverageFrom` in `jest.config.ts`; the `(examples)`
33
+ showcase pages are excluded).
@@ -0,0 +1,58 @@
1
+ # Testing
2
+
3
+ Principles shared by every stack. Tooling, file patterns and tiers in use are
4
+ in `stack/testing.md`.
5
+
6
+ ## Tiers
7
+
8
+ | Tier | Scope | Runs in |
9
+ | ----------- | -------------------------------------------------------------- | --------------------------------- |
10
+ | Unit | One unit in isolation; no network, no database | `test` gate (Stop hook, loop, CI) |
11
+ | Integration | Adapters against real dependencies (PostgreSQL in a container) | extra gate (loop, CI) |
12
+ | E2E | Full flows through the public interface (HTTP API, UI) | extra gate (loop, CI) |
13
+
14
+ A tier the stack does not use yet is listed as **pending** in
15
+ `stack/testing.md`; the first change that needs it sets it up.
16
+
17
+ ## Rules
18
+
19
+ - Each spec scenario (`#### Scenario:` in OpenSpec) has at least one test.
20
+ Put the scenario name in the test title.
21
+ - Write the failing test first, then the code.
22
+ - Never change what an existing test asserts to make it pass. A conflict
23
+ between a test and the code is reported to a human; a confirmed
24
+ out-of-scope bug is recorded with `it.failing` and an exact assertion.
25
+ - Test data comes from factories, not ad-hoc literals repeated across tests.
26
+ - Access control: every protected route tests both the allowed and the
27
+ denied path.
28
+ - Deterministic: no real clock or randomness without injection. Flaky tests
29
+ are bugs and block the gate.
30
+
31
+ ## Coverage
32
+
33
+ - **Measured over all source files**, not only the ones some test imports:
34
+ untested modules must count as uncovered (`collectCoverageFrom`). The only
35
+ exclusions allowed are tests and their helpers, generated code, declaration
36
+ files, `*.example.*` reference files and template showcase code; each one is
37
+ listed in the coverage configuration.
38
+ - **Executable statements, not lines of text**: use the statement-based
39
+ (istanbul / `babel`) provider, so type-only files weigh nothing.
40
+ - **Floor: 85% on lines, statements, branches and functions**, in every
41
+ stack, enforced by the `test` gate. A product may raise it, never lower it;
42
+ lowering it needs an ADR.
43
+
44
+ ## Existing projects
45
+
46
+ An existing project below 85% enters the harness with a **coverage ratchet**
47
+ (ADR 0009):
48
+
49
+ 1. Measure coverage honestly (all source files, statement-based) and set the
50
+ gate floor to that baseline. `.harness/autonomy` is `disabled`.
51
+ 2. Add **characterization tests** for the critical flows, run with
52
+ `run-tasks.sh --force` under supervision. They pin current behaviour, even
53
+ if wrong; suspicious behaviour gets a `// Characterization: looks like a
54
+ bug — …` comment and is reported, never fixed in the same change.
55
+ 3. Set `.harness/autonomy` to `enabled`.
56
+ 4. After every merged change, raise the floor to the new measured coverage.
57
+ The floor never goes down; 85% on every metric is the target, after which
58
+ the shared floor applies.
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code Stop hook: blocks the agent from finishing while DoD gates fail.
3
+ #
4
+ # - No uncommitted changes → nothing to verify, allow stop.
5
+ # - Gates fail → exit 2; stderr is fed back to the agent so it keeps working.
6
+ # - After HARNESS_STOP_MAX_BLOCKS consecutive blocks in one session, allow the
7
+ # stop so a human (or the task loop's retry logic) can take over.
8
+ set -uo pipefail
9
+
10
+ input="$(cat)"
11
+ project_dir="${CLAUDE_PROJECT_DIR:-$(pwd)}"
12
+ cd "$project_dir" || exit 0
13
+
14
+ gates="$project_dir/.harness/gates.sh"
15
+ [[ -x "$gates" ]] || exit 0
16
+ [[ -n "$(git status --porcelain 2>/dev/null)" ]] || exit 0
17
+
18
+ session_id="$(printf '%s' "$input" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)"
19
+ state_dir="$project_dir/.harness/state"
20
+ mkdir -p "$state_dir"
21
+ counter="$state_dir/stop-blocks-${session_id:-unknown}"
22
+ max="${HARNESS_STOP_MAX_BLOCKS:-5}"
23
+
24
+ if output="$("$gates" --stop 2>&1)"; then
25
+ rm -f "$counter"
26
+ exit 0
27
+ fi
28
+
29
+ n=$(( $(cat "$counter" 2>/dev/null || echo 0) + 1 ))
30
+ if (( n > max )); then
31
+ rm -f "$counter"
32
+ echo "Stop hook: gates still failing after $max attempts; allowing stop for human review." >&2
33
+ exit 0
34
+ fi
35
+ echo "$n" > "$counter"
36
+
37
+ {
38
+ echo "Definition of Done not met: gates failed (block $n/$max)."
39
+ echo "Fix the failures below, then finish. Do not weaken, skip or delete tests to pass."
40
+ echo
41
+ echo "$output"
42
+ } >&2
43
+ exit 2
@@ -0,0 +1,72 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
+ "permissions": {
4
+ "allow": [
5
+ "Bash(.harness/gates.sh:*)",
6
+ "Bash(.harness/status.sh:*)",
7
+ "Bash(.harness/loop.sh:*)",
8
+ "Bash(.harness/spec-pr.sh:*)",
9
+ "Bash(.harness/openspec.sh:*)",
10
+ "Bash(git status:*)",
11
+ "Bash(git diff:*)",
12
+ "Bash(git log:*)",
13
+ "Bash(git show:*)",
14
+ "Bash(openspec list:*)",
15
+ "Bash(openspec show:*)",
16
+ "Bash(openspec validate:*)",
17
+ "Bash(node_modules/.bin/openspec:*)",
18
+ "Bash(gh pr view:*)",
19
+ "Bash(gh pr list:*)",
20
+ "Bash(pnpm test:*)",
21
+ "Bash(pnpm exec jest:*)",
22
+ "Bash(pnpm exec eslint:*)",
23
+ "Bash(pnpm exec tsc:*)",
24
+ "Bash(node_modules/.bin/jest:*)",
25
+ "Bash(node_modules/.bin/eslint:*)",
26
+ "Bash(node_modules/.bin/tsc:*)"
27
+ ],
28
+ "deny": [
29
+ "Read(./.env)",
30
+ "Read(./.env.*)",
31
+ "Edit(./.harness/gates.sh)",
32
+ "Edit(./.harness/gates.conf)",
33
+ "Edit(./.claude/settings.json)",
34
+ "Edit(./.claude/hooks/**)",
35
+ "Bash(git push --force:*)",
36
+ "Bash(git push -f:*)",
37
+ "Edit(./.harness/loop.sh)",
38
+ "Edit(./.harness/notify.sh)",
39
+ "Edit(./.harness/run-tasks.sh)",
40
+ "Edit(./.harness/lib.sh)",
41
+ "Edit(./.harness/spec-pr.sh)",
42
+ "Edit(./.harness/openspec.sh)",
43
+ "Edit(./.harness/pr-scope.sh)",
44
+ "Edit(./.harness/deps.sh)",
45
+ "Edit(./.harness/db-smoke.sh)",
46
+ "Edit(./.harness/models.conf)",
47
+ "Edit(./.harness/models.local.conf)",
48
+ "Edit(./.harness/model-flags.sh)",
49
+ "Skill(opsx:apply)",
50
+ "Skill(skill:opsx:apply)",
51
+ "Skill(openspec-apply-change)",
52
+ "Skill(skill:openspec-apply-change)"
53
+ ]
54
+ },
55
+ "skillOverrides": {
56
+ "opsx:apply": "off",
57
+ "openspec-apply-change": "off"
58
+ },
59
+ "hooks": {
60
+ "Stop": [
61
+ {
62
+ "hooks": [
63
+ {
64
+ "type": "command",
65
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/stop-gates.sh",
66
+ "timeout": 900
67
+ }
68
+ ]
69
+ }
70
+ ]
71
+ }
72
+ }
@@ -0,0 +1,41 @@
1
+ name: gates
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ jobs:
9
+ gates:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ # fetch-depth 2: HEAD is the PR merge commit and HEAD^1 its base, enough
13
+ # to list the files the pull request changes.
14
+ - uses: actions/checkout@v4
15
+ with:
16
+ fetch-depth: 2
17
+
18
+ # TODO: toolchain setup and dependency install for this project's stack
19
+ # (see .harness/standards/stack.md): actions/setup-node, then
20
+ # pnpm/action-setup@v6.1.0 (reads package.json#packageManager; ADR 0014)
21
+ # and `pnpm install --frozen-lockfile`. Every step below needs it: OpenSpec
22
+ # is a pinned devDependency, and a harness installed from the package
23
+ # (ADR 0021) generates .harness/ during the install.
24
+
25
+ - name: Check the harness
26
+ run: if [[ -x node_modules/.bin/harness ]]; then node_modules/.bin/harness check; fi
27
+
28
+ # Spec-only PRs (ADR 0011, 0013) validate the specs instead of running the
29
+ # DoD gates; pr-scope.sh also fails a spec/<id> PR that changes anything
30
+ # outside openspec/changes/<id>/. Pushes to main always run the gates.
31
+ - name: Classify pull request
32
+ id: scope
33
+ run: .harness/pr-scope.sh >>"$GITHUB_OUTPUT"
34
+
35
+ - name: Validate OpenSpec changes and specs
36
+ if: steps.scope.outputs.spec_only == 'true'
37
+ run: .harness/openspec.sh validate --all --strict --no-interactive
38
+
39
+ - name: Run DoD gates
40
+ if: steps.scope.outputs.spec_only != 'true'
41
+ run: .harness/gates.sh