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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +43 -2
  2. package/bin/harness +374 -0
  3. package/package.json +30 -4
  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/dependabot.yml +19 -0
  21. package/templates/.github/workflows/gates.yml +41 -0
  22. package/templates/.harness/db-smoke.sh +75 -0
  23. package/templates/.harness/deps.sh +219 -0
  24. package/templates/.harness/gates.conf.example +31 -0
  25. package/templates/.harness/gates.sh +108 -0
  26. package/templates/.harness/lib.sh +169 -0
  27. package/templates/.harness/loop.sh +169 -0
  28. package/templates/.harness/model-flags.sh +34 -0
  29. package/templates/.harness/models.conf +31 -0
  30. package/templates/.harness/notify.sh +49 -0
  31. package/templates/.harness/openspec.sh +28 -0
  32. package/templates/.harness/pr-scope.sh +44 -0
  33. package/templates/.harness/run-tasks.sh +461 -0
  34. package/templates/.harness/spec-pr.sh +157 -0
  35. package/templates/.harness/status.sh +155 -0
  36. package/templates/CLAUDE.md.tmpl +159 -0
  37. package/templates/openspec/config.yaml +38 -0
  38. package/templates/stacks/nestjs/gates.conf +35 -0
  39. package/templates/stacks/nestjs/gates.yml +71 -0
  40. package/templates/stacks/nextjs/claude-imports.md +4 -0
  41. package/templates/stacks/nextjs/gates.conf +36 -0
  42. package/templates/stacks/nextjs/gates.yml +68 -0
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env bash
2
+ # Shows the state of OpenSpec changes and of the task loop.
3
+ #
4
+ # Usage: .harness/status.sh # overview of every active change
5
+ # .harness/status.sh <change-id> # details: tasks, runner, attempts, cost, last failure
6
+ # .harness/status.sh --usage # machine ledger: cost and first-attempt success
7
+ # # per task type, level and model (ADR 0020)
8
+ #
9
+ # Progress of a change the loop is working on is read from its worktree (ADR 0012),
10
+ # so it does not depend on the branch checked out here.
11
+ set -euo pipefail
12
+
13
+ # shellcheck source-path=SCRIPTDIR source=lib.sh
14
+ . "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
15
+ root="$(harness_root)"
16
+ cd "$root"
17
+
18
+ now=$(date +%s)
19
+ changes_dir="openspec/changes"
20
+
21
+ human_duration() {
22
+ local s=$1
23
+ if (( s >= 3600 )); then printf '%dh%02dm' $((s / 3600)) $((s % 3600 / 60))
24
+ elif (( s >= 60 )); then printf '%dm%02ds' $((s / 60)) $((s % 60))
25
+ else printf '%ds' "$s"; fi
26
+ }
27
+
28
+ # tasks_of <change-id>: prints its tasks.md from the loop worktree, else from the
29
+ # change/<id> branch (worktree already removed), else from this checkout.
30
+ tasks_of() {
31
+ local wt file="$changes_dir/$1/tasks.md"
32
+ wt="$(harness_worktree "$1")"
33
+ if [[ -f "$wt/$file" ]]; then cat "$wt/$file"
34
+ elif git cat-file -e "refs/heads/change/$1:$file" 2>/dev/null; then git show "refs/heads/change/$1:$file"
35
+ elif [[ -f "$file" ]]; then cat "$file"
36
+ else return 1; fi
37
+ }
38
+
39
+ # Changes active here or in a loop worktree, one per line.
40
+ active_changes() {
41
+ local dir wt_parent
42
+ wt_parent="$(dirname "$(harness_worktree x)")"
43
+ for dir in "$changes_dir"/*/ "$wt_parent"/*/"$changes_dir"/*/; do
44
+ [[ -f "$dir/tasks.md" ]] && basename "$dir"
45
+ done | grep -vx archive | sort -u || true
46
+ }
47
+
48
+ # count_tasks <tasks.md content> → "<done> <total>"
49
+ count_tasks() {
50
+ local n_done total
51
+ n_done=$(grep -cE '^[[:space:]]*- \[[xX]\] ' <<<"$1" || true)
52
+ total=$(grep -cE '^[[:space:]]*- \[[ xX]\] ' <<<"$1" || true)
53
+ echo "$n_done $total"
54
+ }
55
+
56
+ # Reads the runner state file into state_* variables; returns 1 if absent.
57
+ load_state() {
58
+ local file=".harness/state/run-$1" key value
59
+ state_status="" state_pid="" state_task_id="" state_task="" state_attempt="" state_max_attempts=""
60
+ # shellcheck disable=SC2034 # state_updated is part of the file format, unused here
61
+ state_started="" state_attempt_started="" state_updated=""
62
+ [[ -f "$file" ]] || return 1
63
+ while IFS='=' read -r key value; do
64
+ printf -v "state_$key" '%s' "$value"
65
+ done <"$file"
66
+ # A "running" state whose process is gone was killed without cleanup.
67
+ if [[ "$state_status" == running ]] && ! kill -0 "$state_pid" 2>/dev/null; then
68
+ state_status="stale"
69
+ fi
70
+ }
71
+
72
+ total_cost() {
73
+ local dir=".harness/logs/$1"
74
+ [[ -d "$dir" ]] || { echo "0.00"; return; }
75
+ cat "$dir"/task-*-attempt-*.json 2>/dev/null \
76
+ | grep -o '"total_cost_usd":[0-9.]*' | cut -d: -f2 \
77
+ | awk '{ s += $1 } END { printf "%.2f", s }'
78
+ }
79
+
80
+ overview() {
81
+ local found=0 change counts
82
+ printf '%-36s %-8s %-12s %s\n' CHANGE TASKS LOOP COST
83
+ while IFS= read -r change; do
84
+ [[ -n "$change" ]] || continue
85
+ found=1
86
+ read -r d t <<<"$(count_tasks "$(tasks_of "$change")")"
87
+ counts="$d/$t"
88
+ if load_state "$change"; then loop="$state_status"; else loop="-"; fi
89
+ printf '%-36s %-8s %-12s US$ %s\n' "$change" "$counts" "$loop" "$(total_cost "$change")"
90
+ done < <(active_changes)
91
+ (( found )) || echo "(no active changes in $changes_dir)"
92
+ }
93
+
94
+ details() {
95
+ local change="$1" tasks log_dir=".harness/logs/$1"
96
+ tasks="$(tasks_of "$1")" || { echo "status: no change $1 (no $changes_dir/$1/tasks.md here, in its worktree or on change/$1)" >&2; exit 1; }
97
+
98
+ read -r d t <<<"$(count_tasks "$tasks")"
99
+ echo "Change: $change ($d/$t tasks done, US\$ $(total_cost "$change") spent)"
100
+ echo
101
+ grep -E '^[[:space:]]*- \[[ xX]\] ' <<<"$tasks" | sed -E \
102
+ -e 's/^[[:space:]]*- \[[xX]\] / [x] /' -e 's/^[[:space:]]*- \[ \] / [ ] /' \
103
+ | cut -c1-100
104
+ echo
105
+
106
+ if load_state "$change"; then
107
+ echo "Loop: $state_status (started $(human_duration $((now - state_started))) ago)"
108
+ if [[ "$state_status" == running || "$state_status" == stale ]]; then
109
+ echo " task $state_task_id, attempt $state_attempt/$state_max_attempts," \
110
+ "running for $(human_duration $((now - state_attempt_started)))"
111
+ echo " $state_task"
112
+ fi
113
+ else
114
+ echo "Loop: never run"
115
+ fi
116
+
117
+ if [[ -f "$log_dir/summary.tsv" ]] && (( $(wc -l <"$log_dir/summary.tsv") > 1 )); then
118
+ echo
119
+ echo "Attempts:"
120
+ tail -n +2 "$log_dir/summary.tsv" | while IFS=$'\t' read -r ts task attempt dur result level model effort cost _; do
121
+ printf ' %-6s attempt %-2s %-5s %8s %s' "$task" "$attempt" "$result" "$(human_duration "$dur")" "${ts:0:19}"
122
+ [[ -n "${level:-}" ]] && printf ' %s %s/%s US$ %s' "$level" "$model" "$effort" "$cost"
123
+ echo
124
+ done
125
+ fi
126
+
127
+ local last_fail
128
+ last_fail="$(ls -t "$log_dir"/task-*-attempt-*.gates.log 2>/dev/null | head -n1 || true)"
129
+ if [[ -n "$last_fail" ]]; then
130
+ echo
131
+ echo "Last failure: $last_fail"
132
+ grep -E '^(FAIL|gates:)' "$last_fail" | sed 's/^/ /'
133
+ fi
134
+ }
135
+
136
+ # usage: aggregates the machine-wide ledger (ADR 0020, calibration).
137
+ usage() {
138
+ local ledger="${HARNESS_USAGE_LEDGER:-$HOME/.local/state/harness/usage.tsv}"
139
+ [[ -s "$ledger" ]] || { echo "(no sessions recorded in $ledger)"; return 0; }
140
+ printf '%-16s %-5s %-30s %8s %10s %10s %s\n' TASK LEVEL MODEL SESSIONS "COST US\$" "AVG US\$" "1ST-ATTEMPT PASS"
141
+ tail -n +2 "$ledger" | awk -F'\t' '
142
+ { k = $4 "\t" $5 "\t" $6; n[k]++; c[k] += $8
143
+ if ($12 == 1 && $11 != "fallback") { first[k]++; if ($11 == "pass" || $11 == "fixed" || $11 == "replied") ok[k]++ } }
144
+ END { for (k in n) { split(k, f, "\t")
145
+ rate = first[k] ? sprintf("%d%% (%d/%d)", 100 * ok[k] / first[k], ok[k], first[k]) : "-"
146
+ printf "%-16s %-5s %-30s %8d %10.2f %10.2f %s\n", f[1], f[2], f[3], n[k], c[k], c[k] / n[k], rate } }' | sort
147
+ printf '%-16s %-5s %-30s %8s %10.2f\n' total "" "" "$(( $(wc -l <"$ledger") - 1 ))" \
148
+ "$(tail -n +2 "$ledger" | awk -F'\t' '{ s += $8 } END { print s + 0 }')"
149
+ }
150
+
151
+ case "${1:-}" in
152
+ "") overview ;;
153
+ --usage) usage ;;
154
+ *) details "$1" ;;
155
+ esac
@@ -0,0 +1,159 @@
1
+ # CLAUDE.md — `{{PROJECT_NAME}}`
2
+
3
+ <!-- Project-specific context goes in the "Project" section at the end.
4
+ Everything above it comes from agent-harness (version in
5
+ .harness/VERSION). -->
6
+
7
+ ## Language rule (mandatory)
8
+
9
+ - **Every artifact you produce MUST be in English**: code, identifiers,
10
+ comments, docs, specs, commit messages, PR text, logs.
11
+ - **Chat is the only exception**: reply in the same language the user writes in.
12
+
13
+ ## Standards
14
+
15
+ Follow these; they are the source of truth for how this product is built.
16
+ Shared standards are in `.harness/standards/`; the ones specific to this
17
+ product's stack are in `.harness/standards/stack/`.
18
+
19
+ {{STANDARDS_IMPORTS}}
20
+
21
+ ## Workflow: Spec-Driven Development (OpenSpec)
22
+
23
+ The OpenSpec CLI is a pinned devDependency, never a global install: wherever
24
+ an OpenSpec workflow or skill says `openspec <args>`, run
25
+ `.harness/openspec.sh <args>` instead.
26
+
27
+ 1. **Propose**: every non-trivial change starts as an OpenSpec change under
28
+ `openspec/changes/<change-id>/` (`proposal.md`, delta `specs/`, optional
29
+ `design.md`, `tasks.md`), e.g. with `/opsx:propose`. Write only those
30
+ files; nothing else in the repository changes in a proposal.
31
+ 2. **Spec PR (last step of proposing)**: run `.harness/spec-pr.sh <change-id>`.
32
+ It validates the change (`--strict`), moves it to branch `spec/<change-id>`
33
+ from `main`, refuses files outside the change directory, commits, pushes
34
+ and opens (or updates) the PR `spec: <change-id>`, printing its URL. CI
35
+ validates a spec PR with OpenSpec instead of the gates.
36
+ 3. **Human checkpoint**: the user reviews and comments on the spec PR.
37
+ Address comments by editing the change on `spec/<change-id>` and running
38
+ `.harness/spec-pr.sh <change-id>` again. **Merging the spec PR is the
39
+ approval**; never merge it yourself and never implement before it is
40
+ merged. `.harness/run-tasks.sh` refuses a change that is not on `main`.
41
+ 4. **Apply**: only the task loop implements a change:
42
+ `.harness/loop.sh start <change-id>` (or `.harness/run-tasks.sh`), one
43
+ fresh session per task, on a new branch `change/<change-id>` created from
44
+ `main`; it opens the implementation PR linking the spec PR. Do not
45
+ implement tasks in an interactive session: `/opsx:apply` and the
46
+ `openspec-apply-change` skill are disabled in `.claude/settings.json`. Do
47
+ not edit the approved spec while implementing; a spec change needs a new
48
+ spec PR.
49
+ 5. **Archive**: after the implementation PR is merged, the change is archived
50
+ on branch `archive/<change-id>` and the PR `archive: <change-id>` merges
51
+ its delta into `openspec/specs/`. The PR watcher does it by itself (ADR
52
+ 0022): `openspec archive <id> --yes`, the formatter on `openspec/`
53
+ (`FORMAT_SPECS` in `.harness/gates.conf`; the archive output is not
54
+ formatted) and `validate --all --strict`. Where there is no watcher, do
55
+ the same by hand, in that order, on that branch. Merging the archive PR is
56
+ the user's action. If `validate --strict` fails after the archive because a
57
+ merged requirement is over 500 characters, the fix is editorial: split the
58
+ requirement or move detail into scenarios, without changing behaviour, and
59
+ list each edit in the PR description.
60
+
61
+ **Dependencies between changes.** A change that builds on requirements another
62
+ change adds or modifies lists it in `proposal.md` under an optional
63
+ `## Depends on` section: one change id in backticks per bullet, e.g.
64
+ ``- `role-deactivation-guard` ``. `.harness/spec-pr.sh` checks that each id is
65
+ a change (open, merged or archived); the watcher starts the change only after
66
+ those are archived and archives in that order. Use `MODIFIED` on an existing
67
+ requirement instead of `ADDED` next to it. Otherwise the archived spec ends up
68
+ with contradicting requirements that `openspec validate` does not detect.
69
+
70
+ **Whoever writes a spec simulates its archive.** The merged specs must still
71
+ pass `.harness/openspec.sh validate --all --strict` after the archive, and a
72
+ requirement over 500 characters, or a `MODIFIED` one that is not in
73
+ `openspec/specs/` yet, only fails then. `.harness/spec-pr.sh` runs that
74
+ simulation in a scratch copy of `openspec/` (archiving first the unarchived
75
+ changes listed under `## Depends on`) and refuses the spec PR if it fails: fix
76
+ the spec before opening the PR, keeping each requirement under 500 characters
77
+ and the detail in scenarios.
78
+
79
+ Write tasks so each one is small, independently verifiable by the gates, and
80
+ ends in a green state. The loop picks the model from the task's reasoning
81
+ level (ADR 0020): mark a task `<!-- level: R3 -->` at the end of its line when
82
+ it needs design judgement the spec cannot fully pin down, `<!-- level: R1 -->`
83
+ when it is a mechanical edit, and nothing otherwise (R2).
84
+
85
+ **Dependencies (ADR 0015).** A change that adds packages declares them in
86
+ `design.md` under `## Dependencies`, one bullet each, with exact versions:
87
+ `- dependency: <name>@<x.y.z>`, `- devDependency: <name>@<x.y.z>`, and
88
+ `- allow-build: <name>` only for a package that must run its install script.
89
+ The reviewer approves them with the spec. The task loop installs exactly these
90
+ before the first task (`.harness/deps.sh`), denies any other install script,
91
+ and refuses changes to other dependencies. No task adds packages, and an agent
92
+ in the loop never edits `package.json`, the lockfile or `pnpm-workspace.yaml`:
93
+ a missing package escalates and needs a spec change.
94
+
95
+ ## Definition of Done
96
+
97
+ - Done means `.harness/gates.sh` exits 0. Your own assessment is not enough.
98
+ - A `Stop` hook runs the gates when you finish with uncommitted changes and
99
+ sends failures back to you.
100
+ - Never make gates pass by deleting, skipping or weakening tests, or by
101
+ editing `.harness/gates.*` or `.claude/`.
102
+ - Never invert the intent of an existing test to match the code. If an
103
+ existing test conflicts with the code or cannot pass honestly, stop and
104
+ report the conflict; the human decides which one is wrong. A confirmed bug
105
+ that is out of scope is recorded with `it.failing` and an exact assertion.
106
+
107
+ ## When the user talks to you through a channel (Telegram)
108
+
109
+ You are the session the user reaches from the channel. You plan and start
110
+ work; the task loop implements it.
111
+
112
+ - A request for a change becomes an OpenSpec change (`/opsx:propose`) and
113
+ ends with `.harness/spec-pr.sh <change-id>` (workflow steps 1–2). Reply in
114
+ the channel with the PR link and a short summary: why, the requirements and
115
+ scenarios, the tasks, and any behaviour change. Then wait.
116
+ - Review comments on the spec PR or in the channel are addressed on
117
+ `spec/<change-id>` and pushed with `.harness/spec-pr.sh <change-id>`; reply
118
+ with what changed.
119
+ - Merging the spec PR starts the work: the spec PR watcher (ADR 0017) runs
120
+ `.harness/loop.sh start <change-id>` and notifies the channel, and it also
121
+ addresses the user's review comments on the spec PR by itself. With
122
+ `HARNESS_WATCH_PR_HEALTH=fix` it also fixes failing checks and conflicts on
123
+ every PR and handles review comments on implementation and sync PRs
124
+ (ADR 0019). Do not start the loop again or address those comments in this
125
+ session. If the user asks
126
+ you to start and `.harness/loop.sh status <change-id>` shows no loop (no
127
+ watcher on this machine), check the merge with `gh pr view spec/<change-id>`
128
+ and run `.harness/loop.sh start <change-id>`, then reply that it started.
129
+ The loop works in its own git worktree, so you can keep writing other specs
130
+ here meanwhile; never edit that worktree yourself. Never merge the spec PR,
131
+ never implement the tasks yourself in this session, and never approve on
132
+ the user's behalf.
133
+ - The loop reports progress to the channel by itself (task passed, paused by
134
+ the usage limit, escalated, done with the PR link). When asked for status,
135
+ answer from `.harness/loop.sh status <change-id>`.
136
+ - If the loop escalates, read `.harness/status.sh <change-id>` and the last
137
+ gate failure, explain the cause in the channel and propose the next step.
138
+ The work in progress stays uncommitted in the loop worktree (path in the
139
+ runner log); the loop refuses to restart until it is finished or discarded.
140
+ - After the user merges the implementation PR, the watcher opens the archive
141
+ PR (workflow step 5) and the channel is told. Without a watcher on this
142
+ machine, archive the change by hand in branch `archive/<change-id>`, as step
143
+ 5 says.
144
+ - Every comment you post on a pull request ends with the line
145
+ `<!-- harness-agent -->`: GitHub may show it under the user's account, and
146
+ the watcher ignores marked comments instead of treating them as review.
147
+
148
+ ## When running inside the task loop
149
+
150
+ The loop prompt tells you when you are in it. In that case:
151
+
152
+ - Implement **only** the assigned task. Do not start other tasks.
153
+ - Do **not** edit `tasks.md`, commit, or push; the loop does that.
154
+ - If the task is impossible or the spec is ambiguous, stop and explain why in
155
+ your final message instead of guessing.
156
+
157
+ ## Project
158
+
159
+ <!-- Architecture overview, key commands, domain vocabulary, gotchas. -->
@@ -0,0 +1,38 @@
1
+ schema: spec-driven
2
+
3
+ context: |
4
+ Language: English. All artifacts must be written in English.
5
+ Keep OpenSpec structural headings and SHALL/MUST keywords in English.
6
+
7
+ This project follows the agent-harness standards. Read them before writing
8
+ any artifact and do not contradict them:
9
+ - .harness/standards/*.md shared: conventions, testing, dod, stack
10
+ - .harness/standards/stack/*.md this product's stack: stack, architecture,
11
+ conventions, testing
12
+ Done means `.harness/gates.sh` exits 0 (see dod.md).
13
+
14
+ rules:
15
+ proposal:
16
+ - Include a "Non-goals" section.
17
+ - Call out any behaviour change to existing code explicitly.
18
+ specs:
19
+ - Every requirement has at least one "#### Scenario:" that maps to an automated test.
20
+ - Security-relevant requirements include a negative scenario (what must be rejected).
21
+ - To change an existing requirement use MODIFIED with its full text; do not ADD a contradicting one.
22
+ design:
23
+ - "Packages the change adds go in a '## Dependencies' section, one bullet each: '- dependency: <name>@<x.y.z>' or '- devDependency: <name>@<x.y.z>' (exact versions; '- allow-build: <name>' only when a package must run its install script). The task loop installs exactly these; anything else escalates."
24
+ tasks:
25
+ - Do not write tasks that add, remove or update packages; the loop installs the dependencies declared in design.md before the first task.
26
+ - 'Mark a task <!-- level: R3 --> at the end of its line when it needs design judgement the spec cannot fully pin down, <!-- level: R1 --> when it is a mechanical edit; leave it out otherwise (R2). The loop picks the model from the level (ADR 0020).'
27
+ - Use checkbox lines "- [ ] N.M <description>"; the task loop parses this exact format.
28
+ - Start each task with a short imperative title sentence; it becomes the commit subject.
29
+ - Each task is small, independently implementable, and leaves every gate green.
30
+ - Each task names the files it touches and the tests it adds.
31
+ - Do not include tasks for manual QA, deployment or archiving.
32
+
33
+ operations:
34
+ apply:
35
+ guidance:
36
+ - Only the harness task loop applies changes (.harness/loop.sh start <change-id>), after the spec PR is merged.
37
+ - Implement one task at a time; run .harness/gates.sh before finishing.
38
+ - Never change what an existing test asserts; report conflicts instead.
@@ -0,0 +1,35 @@
1
+ # Gate commands for a NestJS service created from data-matters-io/nestjs-new-app.
2
+ # Sourced by .harness/gates.sh.
3
+ #
4
+ # Gates call node_modules/.bin directly instead of `pnpm exec`: pnpm 11 runs a
5
+ # dependency check before `exec` that may trigger `pnpm install`, and a gate must
6
+ # never install or mutate anything. Dependencies are installed beforehand
7
+ # (`make setup` locally, `pnpm install --frozen-lockfile` in CI).
8
+ bin="node_modules/.bin"
9
+
10
+ GATE_SETUP="$bin/prisma generate"
11
+
12
+ # Coverage floor: set just below the template's measured coverage. Raise it
13
+ # as coverage grows; never lower it (standards/testing.md).
14
+ GATE_TEST="$bin/jest --silent --coverage --coverageThreshold='{\"global\":{\"lines\":85,\"statements\":85,\"branches\":85,\"functions\":85}}'"
15
+ # Read-only and zero warnings: warnings still count as not done.
16
+ GATE_LINT="$bin/eslint . --max-warnings 0"
17
+ # The root tsconfig also covers tests (tsconfig.build.json excludes them).
18
+ GATE_TYPECHECK="$bin/tsc --noEmit -p tsconfig.json"
19
+ GATE_BUILD="$bin/nest build"
20
+
21
+ # Formats openspec/ after an archive (the archive output is not formatted).
22
+ FORMAT_SPECS="$bin/prettier --write openspec"
23
+
24
+ # Stop hook runs the fast gates; e2e and integration run in the loop and CI.
25
+ STOP_GATES=(lint typecheck test)
26
+
27
+ GATE_EXTRA=(
28
+ "e2e:NODE_OPTIONS=--experimental-vm-modules $bin/jest --silent --config ./test/jest-e2e.json --runInBand"
29
+ # Integration tests start PostgreSQL with Testcontainers: needs Docker or Podman.
30
+ "integration:NODE_OPTIONS=--experimental-vm-modules $bin/jest --silent --config ./test/jest-integration.json --runInBand"
31
+ # Database smoke (ADR 0016): migrations and seed on an empty PostgreSQL, through
32
+ # the app's Prisma setup. Throwaway container locally (HARNESS_SKIP_GATES=db
33
+ # without one); the postgres service of gates.yml in CI.
34
+ "db:.harness/db-smoke.sh '$bin/prisma migrate deploy' '$bin/prisma db seed'"
35
+ )
@@ -0,0 +1,71 @@
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
+ timeout-minutes: 20
12
+ # PostgreSQL for the db gate (.harness/db-smoke.sh, ADR 0016): migrations and
13
+ # seed run here, inside the required gates job.
14
+ services:
15
+ postgres:
16
+ image: postgres:16-alpine
17
+ env:
18
+ POSTGRES_USER: smoke
19
+ POSTGRES_PASSWORD: smoke
20
+ POSTGRES_DB: smoke
21
+ ports:
22
+ - 5432:5432
23
+ options: >-
24
+ --health-cmd "pg_isready -U smoke -d smoke"
25
+ --health-interval 5s
26
+ --health-timeout 5s
27
+ --health-retries 20
28
+ env:
29
+ HARNESS_DB_SMOKE_URL: postgresql://smoke:smoke@localhost:5432/smoke
30
+ steps:
31
+ # fetch-depth 2: HEAD is the PR merge commit and HEAD^1 its base, enough
32
+ # to list the files the pull request changes.
33
+ - uses: actions/checkout@v4
34
+ with:
35
+ fetch-depth: 2
36
+
37
+ # Reads the exact pnpm version from package.json#packageManager (ADR 0014).
38
+ # pnpm 12 needs v6.1.0+; the floating @v6 tag is older.
39
+ - uses: pnpm/action-setup@v6.1.0
40
+
41
+ - uses: actions/setup-node@v4
42
+ with:
43
+ node-version: 22
44
+ cache: pnpm
45
+
46
+ - name: Install dependencies
47
+ run: pnpm install --frozen-lockfile
48
+
49
+ # A harness installed from the package (ADR 0021) is generated by the
50
+ # install's "prepare"; a missing or stale one fails here.
51
+ - name: Check the harness
52
+ run: if [[ -x node_modules/.bin/harness ]]; then node_modules/.bin/harness check; fi
53
+
54
+ # After the install: with the package, .harness/ is generated by it.
55
+ # Spec-only PRs (ADR 0011, 0013) validate the specs instead of running the
56
+ # DoD gates; pr-scope.sh also fails a spec/<id> PR that changes anything
57
+ # outside openspec/changes/<id>/. Pushes to main always run the gates.
58
+ - name: Classify pull request
59
+ id: scope
60
+ run: .harness/pr-scope.sh >>"$GITHUB_OUTPUT"
61
+
62
+ # OpenSpec is a pinned devDependency (.harness/openspec.sh).
63
+ - name: Validate OpenSpec changes and specs
64
+ if: steps.scope.outputs.spec_only == 'true'
65
+ run: .harness/openspec.sh validate --all --strict --no-interactive
66
+
67
+ # The integration gate starts PostgreSQL with Testcontainers, using the
68
+ # runner's Docker daemon.
69
+ - name: Run DoD gates
70
+ if: steps.scope.outputs.spec_only != 'true'
71
+ run: .harness/gates.sh
@@ -0,0 +1,4 @@
1
+ Next.js rules maintained by `next dev` in `AGENTS.md` (ADR 0018); read them
2
+ before writing Next.js code:
3
+
4
+ @AGENTS.md
@@ -0,0 +1,36 @@
1
+ # Gate commands for a Next.js product created from data-matters-io/nextjs-new-app.
2
+ # Sourced by .harness/gates.sh. Validated against nextjs-new-app@61c7a10.
3
+
4
+ # Gates call node_modules/.bin directly instead of `pnpm exec`: pnpm 11 runs a
5
+ # dependency check before `exec` that may trigger `pnpm install`, and a gate must
6
+ # never install or mutate anything. Dependencies are installed beforehand
7
+ # (`make install` locally, `pnpm install --frozen-lockfile` in CI).
8
+ bin="node_modules/.bin"
9
+
10
+ # `prisma generate` resolves DATABASE_URL from the config even though it never
11
+ # connects; a placeholder keeps gates working in clean environments (CI, VPS, loop).
12
+ export DATABASE_URL="${DATABASE_URL:-postgresql://gate:gate@localhost:5432/gate}"
13
+ GATE_SETUP="$bin/prisma generate"
14
+
15
+ # Coverage floor: set just below the template's measured coverage. Raise it
16
+ # as coverage grows; never lower it (standards/testing.md).
17
+ GATE_TEST="$bin/jest --silent --coverageThreshold='{\"global\":{\"lines\":85,\"statements\":85,\"branches\":85,\"functions\":85}}'"
18
+ # Read-only and zero warnings: warnings are style/quality rules from
19
+ # @data-matters.io/eslint-config and still count as not done.
20
+ GATE_LINT="$bin/eslint . --max-warnings 0"
21
+ GATE_TYPECHECK="$bin/tsc --noEmit"
22
+ GATE_BUILD="$bin/next build"
23
+
24
+ # Formats openspec/ after an archive (the archive output is not formatted).
25
+ FORMAT_SPECS="$bin/prettier --write openspec"
26
+
27
+ # Stop hook runs the fast gates (~40s); build (~60s) runs in the loop and CI.
28
+ STOP_GATES=(lint typecheck test)
29
+
30
+ GATE_EXTRA=(
31
+ # Database smoke (ADR 0016): migrations and seed on an empty PostgreSQL, through
32
+ # the app's Prisma setup. Throwaway container locally (HARNESS_SKIP_GATES=db
33
+ # without one); the postgres service of gates.yml in CI.
34
+ "db:.harness/db-smoke.sh '$bin/prisma migrate deploy' '$bin/prisma db seed'"
35
+ # "security:pnpm audit --audit-level high"
36
+ )
@@ -0,0 +1,68 @@
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
+ # PostgreSQL for the db gate (.harness/db-smoke.sh, ADR 0016): migrations and
12
+ # seed run here, inside the required gates job.
13
+ services:
14
+ postgres:
15
+ image: postgres:16-alpine
16
+ env:
17
+ POSTGRES_USER: smoke
18
+ POSTGRES_PASSWORD: smoke
19
+ POSTGRES_DB: smoke
20
+ ports:
21
+ - 5432:5432
22
+ options: >-
23
+ --health-cmd "pg_isready -U smoke -d smoke"
24
+ --health-interval 5s
25
+ --health-timeout 5s
26
+ --health-retries 20
27
+ env:
28
+ HARNESS_DB_SMOKE_URL: postgresql://smoke:smoke@localhost:5432/smoke
29
+ steps:
30
+ # fetch-depth 2: HEAD is the PR merge commit and HEAD^1 its base, enough
31
+ # to list the files the pull request changes.
32
+ - uses: actions/checkout@v4
33
+ with:
34
+ fetch-depth: 2
35
+
36
+ # Reads the exact pnpm version from package.json#packageManager (ADR 0014).
37
+ # pnpm 12 needs v6.1.0+; the floating @v6 tag is older.
38
+ - uses: pnpm/action-setup@v6.1.0
39
+
40
+ - uses: actions/setup-node@v4
41
+ with:
42
+ node-version: 24
43
+ cache: pnpm
44
+
45
+ - name: Install dependencies
46
+ run: pnpm install --frozen-lockfile
47
+
48
+ # A harness installed from the package (ADR 0021) is generated by the
49
+ # install's "prepare"; a missing or stale one fails here.
50
+ - name: Check the harness
51
+ run: if [[ -x node_modules/.bin/harness ]]; then node_modules/.bin/harness check; fi
52
+
53
+ # After the install: with the package, .harness/ is generated by it.
54
+ # Spec-only PRs (ADR 0011, 0013) validate the specs instead of running the
55
+ # DoD gates; pr-scope.sh also fails a spec/<id> PR that changes anything
56
+ # outside openspec/changes/<id>/. Pushes to main always run the gates.
57
+ - name: Classify pull request
58
+ id: scope
59
+ run: .harness/pr-scope.sh >>"$GITHUB_OUTPUT"
60
+
61
+ # OpenSpec is a pinned devDependency (.harness/openspec.sh).
62
+ - name: Validate OpenSpec changes and specs
63
+ if: steps.scope.outputs.spec_only == 'true'
64
+ run: .harness/openspec.sh validate --all --strict --no-interactive
65
+
66
+ - name: Run DoD gates
67
+ if: steps.scope.outputs.spec_only != 'true'
68
+ run: .harness/gates.sh