@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
package/README.md CHANGED
@@ -1,3 +1,35 @@
1
- # Temporary Holding Version
1
+ # @data-matters.io/agent-harness
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Spec-Driven Development with Claude Code, installed into a product repository:
4
+ the Definition of Done as executable gates, a task loop that implements an
5
+ approved OpenSpec change one task per session, and the standards the agent
6
+ follows.
7
+
8
+ ## Install
9
+
10
+ Pin the package exactly and let the project's install generate the harness:
11
+
12
+ ```sh
13
+ pnpm add -D -E @data-matters.io/agent-harness
14
+ pnpm exec harness init --new --stack nextjs # or nestjs; --existing for a codebase without tests
15
+ ```
16
+
17
+ ```json
18
+ { "scripts": { "prepare": "harness install" } }
19
+ ```
20
+
21
+ `harness install` writes the generated files (`.harness/*.sh`,
22
+ `.harness/standards/`, `.harness/CLAUDE.harness.md`, the Claude Code Stop hook)
23
+ and they stay git-ignored, so they always match the pinned version.
24
+
25
+ ## Commands
26
+
27
+ | Command | What it does |
28
+ |---|---|
29
+ | `harness init` | First adoption: the project-owned and seeded files, `CLAUDE.md`, the `.gitignore` entries |
30
+ | `harness install` | (Re)generates the git-ignored files; idempotent |
31
+ | `harness check` | Fails when the generated files are stale or edited, or a required file is missing |
32
+ | `harness update-files` | Three-way merge of the seeded files (`.claude/settings.json`, `openspec/config.yaml`) after a bump |
33
+ | `harness pr-scope` | Classifies a pull request for CI (spec-only or not) without installing the project |
34
+
35
+ Run `harness --help` for the options.
package/bin/harness ADDED
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env bash
2
+ # The harness CLI (ADR 0021): installs agent-harness into a product repository
3
+ # from the @data-matters.io/agent-harness package pinned in its devDependencies.
4
+ #
5
+ # Usage: harness <command> [options] (run from the product repository root)
6
+ #
7
+ # init (--new | --existing | --template) [--name <name>] [--stack <stack>]
8
+ # first adoption: writes the project-owned and seeded files,
9
+ # the .gitignore entries and CLAUDE.md, then runs `install`.
10
+ # Never overwrites an existing file; conflicts are reported.
11
+ # install (re)generates the git-ignored harness files from this
12
+ # package; idempotent. Run by the project's
13
+ # "prepare": "harness install".
14
+ # check fails when a generated file is missing or differs from this
15
+ # package, a generated file is tracked by git, a .gitignore
16
+ # entry or a required committed file is missing, or a seeded
17
+ # file needs `update-files`. Reports, without failing, drift
18
+ # of the project-owned gate files from the stack preset.
19
+ # update-files three-way merge of the seeded, committed files
20
+ # (.claude/settings.json, openspec/config.yaml) with this
21
+ # package's templates; exits 2 on a conflict.
22
+ # pr-scope runs this package's pr-scope.sh (CI scope job, no install).
23
+ # version prints the package version.
24
+ #
25
+ # Environment:
26
+ # HARNESS_PROJECT_DIR product repository (default: the current directory)
27
+ set -euo pipefail
28
+
29
+ usage() { sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; }
30
+ die() { echo "harness: $*" >&2; exit 1; }
31
+
32
+ pkg="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
33
+ templates="$pkg/templates"
34
+ project="${HARNESS_PROJECT_DIR:-$PWD}"
35
+
36
+ version() {
37
+ local v=""
38
+ [[ -f "$pkg/package.json" ]] \
39
+ && v="$(sed -nE 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$pkg/package.json" | head -n1)"
40
+ [[ -n "$v" ]] || v="$(git -C "$pkg" rev-parse --short HEAD 2>/dev/null || echo unknown)"
41
+ echo "$v"
42
+ }
43
+
44
+ # The .gitignore block for the generated files.
45
+ ignore_entries=(
46
+ ".harness/*.sh"
47
+ ".harness/models.conf"
48
+ ".harness/standards/"
49
+ ".harness/CLAUDE.harness.md"
50
+ ".harness/.generated"
51
+ ".claude/hooks/stop-gates.sh"
52
+ ".harness/state/"
53
+ ".harness/logs/"
54
+ )
55
+ # Seeded files: committed, merged by update-files against the template they
56
+ # were last merged from, kept in .harness/seeded/ (committed too).
57
+ seeded=(.claude/settings.json openspec/config.yaml)
58
+ seed_src() {
59
+ case "$1" in
60
+ .claude/settings.json) echo "$templates/.claude/settings.json" ;;
61
+ openspec/config.yaml) echo "$templates/openspec/config.yaml" ;;
62
+ esac
63
+ }
64
+
65
+ stack_of() { cat "$project/.harness/STACK" 2>/dev/null || true; }
66
+
67
+ # render <dest>: every generated file, as this package produces it for the
68
+ # project's stack. Prints their paths, relative to <dest>.
69
+ render() {
70
+ local dest="$1" stack f
71
+ stack="$(stack_of)"
72
+ mkdir -p "$dest/.harness/standards" "$dest/.claude/hooks"
73
+ for f in "$templates"/.harness/*.sh; do
74
+ cp "$f" "$dest/.harness/$(basename "$f")"
75
+ done
76
+ cp "$templates/.harness/models.conf" "$dest/.harness/models.conf"
77
+ cp "$templates/.claude/hooks/stop-gates.sh" "$dest/.claude/hooks/stop-gates.sh"
78
+ chmod +x "$dest"/.harness/*.sh "$dest"/.claude/hooks/*.sh
79
+ local imports=()
80
+ for f in "$pkg"/standards/*.md; do
81
+ cp "$f" "$dest/.harness/standards/"
82
+ imports+=("@standards/$(basename "$f")")
83
+ done
84
+ if [[ -n "$stack" ]]; then
85
+ [[ -d "$pkg/standards/stacks/$stack" ]] || die "unknown stack '$stack' in .harness/STACK"
86
+ mkdir -p "$dest/.harness/standards/stack"
87
+ for f in "$pkg/standards/stacks/$stack"/*.md; do
88
+ [[ -e "$f" ]] || continue
89
+ cp "$f" "$dest/.harness/standards/stack/"
90
+ imports+=("@standards/stack/$(basename "$f")")
91
+ done
92
+ fi
93
+ render_claude "$stack" "${imports[@]}" >"$dest/.harness/CLAUDE.harness.md"
94
+ (cd "$dest" && find . -type f | sed 's|^\./||' | sort)
95
+ }
96
+
97
+ # render_claude <stack> <import...>: the harness part of CLAUDE.md, i.e.
98
+ # CLAUDE.md.tmpl from its first section to the "## Project" one. Imports are
99
+ # relative to .harness/, where the file lives.
100
+ render_claude() {
101
+ local stack="$1" line on=0; shift
102
+ echo "<!-- Generated by \`harness install\` from @data-matters.io/agent-harness"
103
+ echo " $(version). Do not edit: the next install overwrites it. -->"
104
+ echo
105
+ while IFS= read -r line; do
106
+ [[ "$line" == "## Project" ]] && break
107
+ [[ "$line" == "## "* ]] && on=1
108
+ (( on )) || continue
109
+ if [[ "$line" == "{{STANDARDS_IMPORTS}}" ]]; then
110
+ printf '%s\n' "$@"
111
+ if [[ -n "$stack" && -f "$templates/stacks/$stack/claude-imports.md" ]]; then
112
+ echo
113
+ sed -E 's|^@\.harness/|@|; t; s|^@|@../|' "$templates/stacks/$stack/claude-imports.md"
114
+ fi
115
+ continue
116
+ fi
117
+ printf '%s\n' "$line"
118
+ done <"$templates/CLAUDE.md.tmpl" | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}'
119
+ }
120
+
121
+ initialized() { [[ -f "$project/.harness/autonomy" ]]; }
122
+
123
+ cmd_install() {
124
+ if ! initialized; then
125
+ echo "harness: $project is not initialized (no .harness/autonomy); run \`harness init\`"
126
+ return 0
127
+ fi
128
+ local tmp p
129
+ tmp="$(mktemp -d)"
130
+ render "$tmp/out" >"$tmp/list"
131
+ # Generated files this version no longer ships.
132
+ if [[ -f "$project/.harness/.generated" ]]; then
133
+ while IFS= read -r p; do
134
+ if [[ -n "$p" ]] && ! grep -qxF "$p" "$tmp/list"; then rm -f "$project/$p"; fi
135
+ done <"$project/.harness/.generated"
136
+ fi
137
+ (cd "$tmp/out" && cp -R --preserve=mode . "$project/")
138
+ cp "$tmp/list" "$project/.harness/.generated"
139
+ rm -rf "$tmp"
140
+ echo "harness: agent-harness $(version) installed in $project"
141
+ }
142
+
143
+ cmd_check() {
144
+ local tmp p problems=() notes=() stack src tracked
145
+ for p in .harness/STACK .harness/autonomy .harness/gates.conf .github/workflows/gates.yml CLAUDE.md "${seeded[@]}"; do
146
+ [[ -f "$project/$p" ]] || problems+=("$p: missing (committed file; \`harness init\` writes it)")
147
+ done
148
+ [[ -f "$project/.harness/VERSION" ]] \
149
+ && problems+=(".harness/VERSION: left from sync-project.sh; remove it (the version is the devDependency)")
150
+ if [[ -f "$project/CLAUDE.md" ]] && ! grep -qxF '@.harness/CLAUDE.harness.md' "$project/CLAUDE.md"; then
151
+ problems+=("CLAUDE.md: does not import the harness part (add the line @.harness/CLAUDE.harness.md)")
152
+ fi
153
+ for p in "${ignore_entries[@]}"; do
154
+ grep -qxF "$p" "$project/.gitignore" 2>/dev/null || problems+=(".gitignore: missing entry $p")
155
+ done
156
+ tmp="$(mktemp -d)"
157
+ render "$tmp/out" >"$tmp/list"
158
+ if git -C "$project" rev-parse --git-dir >/dev/null 2>&1; then
159
+ tracked="$(cd "$project" && xargs -r git ls-files -- <"$tmp/list")"
160
+ [[ -z "$tracked" ]] || while IFS= read -r p; do
161
+ problems+=("$p: generated but tracked by git (git rm --cached $p)")
162
+ done <<<"$tracked"
163
+ fi
164
+ while IFS= read -r p; do
165
+ if [[ ! -f "$project/$p" ]]; then problems+=("$p: not installed (run harness install)")
166
+ elif ! cmp -s "$project/$p" "$tmp/out/$p"; then problems+=("$p: differs from agent-harness $(version) (run harness install; never edit it)")
167
+ fi
168
+ done <"$tmp/list"
169
+ for p in "${seeded[@]}"; do
170
+ src="$(seed_src "$p")"
171
+ if [[ ! -f "$project/.harness/seeded/$p" ]]; then problems+=(".harness/seeded/$p: missing merge base (run harness update-files)")
172
+ elif ! cmp -s "$project/.harness/seeded/$p" "$src"; then problems+=("$p: the template changed (run harness update-files and commit the result)")
173
+ fi
174
+ done
175
+ stack="$(stack_of)"
176
+ if [[ -n "$stack" && -d "$templates/stacks/$stack" ]]; then
177
+ for p in .harness/gates.conf:gates.conf .github/workflows/gates.yml:gates.yml; do
178
+ if [[ -f "$project/${p%%:*}" ]] && ! cmp -s "$project/${p%%:*}" "$templates/stacks/$stack/${p#*:}"; then
179
+ notes+=("${p%%:*}: differs from the $stack preset (project-owned; review templates/stacks/$stack/${p#*:})")
180
+ fi
181
+ done
182
+ fi
183
+ rm -rf "$tmp"
184
+ (( ${#notes[@]} == 0 )) || printf 'harness: note: %s\n' "${notes[@]}"
185
+ if (( ${#problems[@]} > 0 )); then
186
+ printf 'harness: %s\n' "${problems[@]}" >&2
187
+ echo "harness: check failed (${#problems[@]} problem(s)) against agent-harness $(version)" >&2
188
+ return 1
189
+ fi
190
+ echo "harness: check passed (agent-harness $(version))"
191
+ }
192
+
193
+ cmd_update_files() {
194
+ local p src base local_file conflicts=0
195
+ for p in "${seeded[@]}"; do
196
+ src="$(seed_src "$p")"
197
+ base="$project/.harness/seeded/$p"
198
+ local_file="$project/$p"
199
+ mkdir -p "$(dirname "$base")" "$(dirname "$local_file")"
200
+ if [[ ! -f "$local_file" ]]; then
201
+ cp "$src" "$local_file"; echo " $p: added"
202
+ elif cmp -s "$local_file" "$src"; then
203
+ :
204
+ elif [[ ! -f "$base" ]]; then
205
+ cp "$src" "$local_file.harness-new"
206
+ echo " $p: CONFLICT (customized, no merge base; new version in $p.harness-new)"
207
+ conflicts=$((conflicts + 1))
208
+ continue
209
+ elif cmp -s "$base" "$src"; then
210
+ continue
211
+ elif cmp -s "$local_file" "$base"; then
212
+ cp "$src" "$local_file"; echo " $p: updated"
213
+ elif git merge-file -L local -L base -L "agent-harness $(version)" "$local_file" "$base" "$src"; then
214
+ echo " $p: merged (local changes kept)"
215
+ else
216
+ echo " $p: CONFLICT (markers in the file)"
217
+ conflicts=$((conflicts + 1))
218
+ fi
219
+ cp "$src" "$base"
220
+ done
221
+ if (( conflicts > 0 )); then
222
+ echo "harness: $conflicts conflict(s) to resolve before committing" >&2
223
+ return 2
224
+ fi
225
+ }
226
+
227
+ cmd_init() {
228
+ local mode="" name="" stack="" conflicts=() gates_conf_src ci_src p
229
+ while [[ $# -gt 0 ]]; do
230
+ case "$1" in
231
+ --new|--existing|--template) mode="${1#--}"; shift ;;
232
+ --name) name="$2"; shift 2 ;;
233
+ --stack) stack="$2"; shift 2 ;;
234
+ *) die "init: unexpected argument: $1" ;;
235
+ esac
236
+ done
237
+ [[ -n "$mode" ]] || usage 1
238
+ [[ ! -f "$project/.harness/VERSION" ]] \
239
+ || die "init: $project has a harness installed by sync-project.sh (.harness/VERSION); migrate it instead"
240
+ gates_conf_src="$templates/.harness/gates.conf.example"
241
+ ci_src="$templates/.github/workflows/gates.yml"
242
+ if [[ -n "$stack" ]]; then
243
+ [[ -d "$templates/stacks/$stack" ]] || die "init: unknown stack '$stack' (available: $(ls "$templates/stacks" | tr '\n' ' '))"
244
+ gates_conf_src="$templates/stacks/$stack/gates.conf"
245
+ ci_src="$templates/stacks/$stack/gates.yml"
246
+ fi
247
+ name="${name:-$(basename "$project")}"
248
+ [[ "$mode" == template ]] && name="__REPO_NAME__"
249
+
250
+ # seed <src> <dest>: writes a committed file unless it exists.
251
+ seed() {
252
+ if [[ -e "$project/$2" ]]; then conflicts+=("$2"); return; fi
253
+ mkdir -p "$(dirname "$project/$2")"
254
+ cp "$1" "$project/$2"
255
+ }
256
+ seed "$gates_conf_src" .harness/gates.conf
257
+ seed "$ci_src" .github/workflows/gates.yml
258
+ for p in "${seeded[@]}"; do
259
+ seed "$(seed_src "$p")" "$p"
260
+ mkdir -p "$(dirname "$project/.harness/seeded/$p")"
261
+ cp "$(seed_src "$p")" "$project/.harness/seeded/$p"
262
+ done
263
+ mkdir -p "$project/.harness"
264
+ echo "$stack" >"$project/.harness/STACK"
265
+ if [[ ! -e "$project/.harness/autonomy" ]]; then
266
+ if [[ "$mode" == existing ]]; then echo disabled; else echo enabled; fi >"$project/.harness/autonomy"
267
+ fi
268
+ if [[ -e "$project/CLAUDE.md" ]]; then
269
+ grep -qxF '@.harness/CLAUDE.harness.md' "$project/CLAUDE.md" \
270
+ || conflicts+=("CLAUDE.md (add the line @.harness/CLAUDE.harness.md above the project section)")
271
+ else
272
+ cat >"$project/CLAUDE.md" <<EOF
273
+ # CLAUDE.md — \`$name\`
274
+
275
+ <!-- The harness part (workflow, Definition of Done, standards) is generated
276
+ by \`harness install\` from @data-matters.io/agent-harness into
277
+ .harness/CLAUDE.harness.md; never copy it here. Project-specific context
278
+ goes in the "Project" section. -->
279
+
280
+ @.harness/CLAUDE.harness.md
281
+
282
+ ## Project
283
+
284
+ <!-- Architecture overview, key commands, domain vocabulary, gotchas. -->
285
+ EOF
286
+ fi
287
+ touch "$project/.gitignore"
288
+ if ! grep -qxF "${ignore_entries[0]}" "$project/.gitignore"; then
289
+ [[ ! -s "$project/.gitignore" ]] || echo >>"$project/.gitignore"
290
+ echo "# agent-harness: generated by \`harness install\` (ADR 0021)" >>"$project/.gitignore"
291
+ fi
292
+ for p in "${ignore_entries[@]}"; do
293
+ grep -qxF "$p" "$project/.gitignore" || echo "$p" >>"$project/.gitignore"
294
+ done
295
+ # Next.js keeps its agent rules in AGENTS.md (ADR 0018).
296
+ if [[ "$stack" == nextjs ]]; then
297
+ echo "AGENTS.md: $("$pkg/scripts/next-agent-rules.sh" "$project")"
298
+ fi
299
+ cmd_install
300
+
301
+ echo "harness: initialized $project ($mode project${stack:+, stack: $stack})"
302
+ if (( ${#conflicts[@]} > 0 )); then
303
+ echo
304
+ echo "Existing files left untouched (merge manually):"
305
+ printf ' - %s\n' "${conflicts[@]}"
306
+ fi
307
+ # shellcheck source=scripts/toolchain.sh
308
+ . "$pkg/scripts/toolchain.sh"
309
+ cat <<EOF
310
+
311
+ Next steps:
312
+ 1. Pin the harness and run it on every install (package.json):
313
+ pnpm add -D -E @data-matters.io/agent-harness@$(version)
314
+ "scripts": { "prepare": "harness install" }
315
+ 2. Review .harness/gates.conf and .github/workflows/gates.yml (project-owned).
316
+ 3. Pin pnpm (ADR 0014): "packageManager": "pnpm@$HARNESS_PNPM_VERSION" and
317
+ devEngines.packageManager { name pnpm, version $HARNESS_PNPM_VERSION, onFail error }.
318
+ Add OpenSpec as a pinned devDependency (version in .harness/openspec.sh).
319
+ 4. Commit the project-owned and seeded files, .harness/seeded/, CLAUDE.md and
320
+ .gitignore; run harness check, then .harness/gates.sh until it is green.
321
+ EOF
322
+ if [[ "$mode" == existing ]]; then
323
+ echo " 5. Add characterization tests for the critical flows, then: echo enabled > .harness/autonomy"
324
+ fi
325
+ }
326
+
327
+ [[ $# -gt 0 ]] || usage 1
328
+ command="$1"; shift
329
+ case "$command" in
330
+ -h|--help|help) usage 0 ;;
331
+ version|--version) version ;;
332
+ init) cmd_init "$@" ;;
333
+ install) [[ $# -eq 0 ]] || usage 1; cmd_install ;;
334
+ check) [[ $# -eq 0 ]] || usage 1; cmd_check ;;
335
+ update-files) [[ $# -eq 0 ]] || usage 1; cmd_update_files ;;
336
+ pr-scope) cd "$project" && exec bash "$templates/.harness/pr-scope.sh" "$@" ;;
337
+ *) echo "harness: unknown command '$command'" >&2; usage 1 ;;
338
+ esac
package/package.json CHANGED
@@ -1,6 +1,31 @@
1
1
  {
2
2
  "name": "@data-matters.io/agent-harness",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Spec-Driven Development with Claude Code: gates, task loop and standards installed into a product repository",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/data-matters-io/data-matters-agent-harness.git"
9
+ },
10
+ "bin": {
11
+ "harness": "bin/harness"
12
+ },
13
+ "files": [
14
+ "bin/harness",
15
+ "templates/CLAUDE.md.tmpl",
16
+ "templates/.claude/",
17
+ "templates/.github/",
18
+ "templates/.harness/",
19
+ "templates/openspec/",
20
+ "templates/stacks/",
21
+ "standards/",
22
+ "scripts/next-agent-rules.sh",
23
+ "scripts/toolchain.sh"
24
+ ],
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "devDependencies": {
29
+ "@changesets/cli": "3.0.3"
30
+ }
6
31
  }
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bash
2
+ # Keeps the Next.js agent-rules block in AGENTS.md, out of CLAUDE.md (ADR 0018).
3
+ #
4
+ # Usage: scripts/next-agent-rules.sh <project-dir>
5
+ #
6
+ # `next dev` (Next.js 16+) writes a managed block between
7
+ # <!-- BEGIN:nextjs-agent-rules --> and <!-- END:nextjs-agent-rules --> when an
8
+ # AI agent runs it: into AGENTS.md when that file exists, otherwise at the end of
9
+ # CLAUDE.md, which the harness renders. This script:
10
+ # - moves a block found in CLAUDE.md into AGENTS.md (created if missing);
11
+ # - creates AGENTS.md when there is none, with the block generated by the
12
+ # project's own Next.js (node_modules/next) or, without it, empty markers
13
+ # that `next dev` fills in;
14
+ # - never changes an AGENTS.md that already holds the block.
15
+ # Prints what it did ("moved", "created", "unchanged"); CLAUDE.md imports
16
+ # AGENTS.md through the harness-rendered nextjs section.
17
+ set -euo pipefail
18
+
19
+ usage() { sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; }
20
+ [[ $# -eq 1 && "$1" != -h && "$1" != --help ]] || usage 1
21
+ dir="$1"
22
+ start='<!-- BEGIN:nextjs-agent-rules -->'
23
+ end='<!-- END:nextjs-agent-rules -->'
24
+ agents="$dir/AGENTS.md"
25
+ claude="$dir/CLAUDE.md"
26
+
27
+ has_block() { [[ -f "$1" ]] && grep -qF "$start" "$1" && grep -qF "$end" "$1"; }
28
+
29
+ if has_block "$claude"; then
30
+ block="$(awk -v s="$start" -v e="$end" 'index($0, s) { on = 1 } on { print } index($0, e) { on = 0 }' "$claude")"
31
+ # Drop the block and the blank lines `next dev` put before it.
32
+ awk -v s="$start" -v e="$end" '
33
+ index($0, s) { skip = 1 }
34
+ !skip { lines[++n] = $0 }
35
+ skip && index($0, e) { skip = 0 }
36
+ END { while (n > 0 && lines[n] == "") n--; for (i = 1; i <= n; i++) print lines[i] }' "$claude" >"$claude.tmp"
37
+ mv "$claude.tmp" "$claude"
38
+ if has_block "$agents"; then
39
+ echo "moved (AGENTS.md already had the block; removed it from CLAUDE.md)"
40
+ else
41
+ if [[ -s "$agents" ]]; then printf '\n%s\n' "$block" >>"$agents"; else printf '%s\n' "$block" >"$agents"; fi
42
+ echo "moved"
43
+ fi
44
+ exit 0
45
+ fi
46
+
47
+ if [[ -f "$agents" ]]; then
48
+ echo "unchanged"
49
+ exit 0
50
+ fi
51
+
52
+ # Ask the project's Next.js for the block of its own version.
53
+ block=""
54
+ if [[ -f "$dir/node_modules/next/dist/server/lib/generate-agent-files.js" ]] && command -v node >/dev/null; then
55
+ tmp="$(mktemp -d)"
56
+ : >"$tmp/AGENTS.md"
57
+ if node -e 'require(process.argv[1]).writeAgentFiles(process.argv[2])' \
58
+ "$dir/node_modules/next/dist/server/lib/generate-agent-files.js" "$tmp" >/dev/null 2>&1 \
59
+ && has_block "$tmp/AGENTS.md"; then
60
+ block="$(sed -e '/./,$!d' "$tmp/AGENTS.md")"
61
+ fi
62
+ rm -rf "$tmp"
63
+ fi
64
+ [[ -n "$block" ]] || block="$start
65
+ $end"
66
+ printf '%s\n' "$block" >"$agents"
67
+ echo "created"
@@ -0,0 +1,14 @@
1
+ # Package-manager versions for every harnessed project and machine (ADR 0014).
2
+ # Sourced by vps-doctor.sh, sync-project.sh and the tests; change them here only,
3
+ # after checking the registry (`npm view pnpm dist-tags.latest`) and the
4
+ # pnpm/action-setup release that supports the pnpm major.
5
+ # shellcheck shell=bash disable=SC2034
6
+
7
+ # Projects: package.json "packageManager" and "devEngines.packageManager", exact.
8
+ HARNESS_PNPM_VERSION="12.11.2"
9
+ # CI: reads the version from packageManager. pnpm 12 needs v6.1.0 or newer;
10
+ # the floating @v6 tag still points to v6.0.10.
11
+ HARNESS_PNPM_ACTION="pnpm/action-setup@v6.1.0"
12
+ # Machines (VPS): npm installed at the user level (npm config get prefix).
13
+ # Projects never run npm: devEngines.packageManager makes it refuse.
14
+ HARNESS_NPM_VERSION="12.2.0"
@@ -0,0 +1,68 @@
1
+ # Conventions
2
+
3
+ Conventions shared by every stack. Stack-specific naming and layout live in
4
+ `stack/conventions.md` (in a product repo, `.harness/standards/stack/`).
5
+
6
+ ## Language
7
+
8
+ - **All artifacts are in English**: code, identifiers, comments, docs, specs,
9
+ ADRs, commit messages, branch names, PR titles/descriptions, logs, file names.
10
+ - User-facing strings go through the stack's i18n mechanism, not hardcoded.
11
+ - **Chat is the only exception**: agents reply in the user's language.
12
+
13
+ ## Formatting and lint
14
+
15
+ Owned entirely by `@data-matters.io/eslint-config`, which also ships `eslint`
16
+ and `prettier` with pinned versions. Do not declare them as direct
17
+ dependencies and do not add local rules that contradict the preset; propose
18
+ changes upstream instead.
19
+
20
+ - pnpm >= 10 no longer hoists their binaries by default: expose them with
21
+ `publicHoistPattern` in `pnpm-workspace.yaml`.
22
+ - Prettier (runs inside ESLint for JS/TS; the `prettier` CLI formats JSON,
23
+ Markdown, YAML and CSS): single quotes, semicolons, trailing commas,
24
+ 2-space indent, 120-column print width, LF.
25
+ - Severity convention: `error` = correctness/security, `warn` =
26
+ style/quality. **The lint gate runs with `--max-warnings 0`**, so warnings
27
+ still block "done".
28
+ - Scripts: `lint` is read-only (CI), `lint:fix` fixes; `format` checks,
29
+ `format:fix` writes.
30
+ - Architecture rules are lint rules from the preset (see
31
+ `stack/architecture.md`); they fail the build instead of relying on review.
32
+ - Pre-commit hook: `data-matters-eslint-init` as `postinstall`. No husky.
33
+ - `.editorconfig` comes from the eslint-config package.
34
+
35
+ ## TypeScript
36
+
37
+ - `strict` mode; no `any` (use `unknown` and narrow). Test files are exempt.
38
+ - Unused parameters/variables must be prefixed with `_`.
39
+ - No non-null assertions (`!`) outside tests.
40
+ - Cross-folder imports use path aliases (`@/…`), never `../../`.
41
+ - Validation at boundaries with Zod; types derive from schemas (`z.infer`) or
42
+ from Prisma rather than being duplicated by hand.
43
+
44
+ ## Commits and branches
45
+
46
+ - [Conventional Commits](https://www.conventionalcommits.org/):
47
+ `<type>(<scope>): <summary>`, imperative, ≤ 72 chars. Breaking changes use
48
+ `!` and a `BREAKING CHANGE:` body.
49
+ - Types: `feat`, `fix`, `refactor`, `test`, `docs`, `chore`, `ci`, `perf`.
50
+ - One task, one commit. In the task loop the runner commits; agents never
51
+ commit or push.
52
+ - `main` is protected; everything lands via PR, and CI must pass.
53
+ - Spec branches: `spec/<openspec-change-id>`, only
54
+ `openspec/changes/<change-id>/`, PR titled `spec: <change-id>`, opened by
55
+ `.harness/spec-pr.sh` (it and CI's `pr-scope.sh` refuse anything else).
56
+ Change branches: `change/<openspec-change-id>`, created from `main` after
57
+ the spec PR is merged. Other work: `<type>/<short-kebab-description>`.
58
+
59
+ ## Comments
60
+
61
+ - Explain _why_, not _what_. Public functions and classes get a short JSDoc.
62
+ - Delete commented-out code. `TODO` references a change id: `TODO(change-id): …`.
63
+
64
+ ## Local workflow
65
+
66
+ `make` is the entry point (`make help`): `make setup` on first run, then the
67
+ stack's dev target, and `make ci-check` for CI parity. Agents use
68
+ `.harness/gates.sh` as the source of truth for "done".
@@ -0,0 +1,53 @@
1
+ # Definition of Done
2
+
3
+ A task is done when **every gate passes**. The agent's own assessment is never
4
+ a completion criterion.
5
+
6
+ ## Human DoD → executable gate
7
+
8
+ | Human criterion | Gate | Required |
9
+ | --------------------------------------------------------------- | --------------------- | --------- |
10
+ | Behaviour matches the spec scenarios; coverage floor holds | `test` | yes |
11
+ | Code follows conventions and architecture rules (zero warnings) | `lint` | yes |
12
+ | No type errors | `typecheck` | yes |
13
+ | The artifact builds | `build` | yes |
14
+ | Adapters work against real dependencies | `integration` (extra) | per stack |
15
+ | Migrations apply and the seed runs on an empty database | `db` (extra) | Prisma |
16
+ | Flows work end to end | `e2e` (extra) | per stack |
17
+ | No known vulnerable dependencies | `security` (extra) | optional |
18
+
19
+ The commands behind each gate are in the stack preset (`.harness/gates.conf`,
20
+ source `templates/stacks/<stack>/gates.conf`) and summarized in
21
+ `stack/stack.md`. `.harness/gates.sh` runs them:
22
+
23
+ | Where | Gates |
24
+ | ----------------------- | -------------------------------------------------- |
25
+ | Claude Code `Stop` hook | `STOP_GATES` (fast profile: lint, typecheck, test) |
26
+ | Task loop | all |
27
+ | CI (`gates.yml`) | all |
28
+
29
+ ## Rules
30
+
31
+ - A required gate that is not configured **fails**. To skip one, set it
32
+ explicitly to `skip:<reason>`; the reason shows up in every gate run.
33
+ - A task is not done if it passes because tests were deleted, weakened or
34
+ skipped, or because lint rules were disabled inline (`eslint-disable`)
35
+ without a justification comment.
36
+ - A task is not done if it changes the intent of an existing test (what it
37
+ asserts) without that change being part of the approved spec.
38
+ - Each task that changes behaviour adds or updates at least one test that
39
+ would fail without the change.
40
+ - Every spec scenario maps to at least one test by the time the change is
41
+ archived.
42
+ - All artifacts are in English (see `conventions.md`). Not machine-checked
43
+ yet; enforced during PR review.
44
+
45
+ ## Human checkpoints
46
+
47
+ 1. **Before execution**: proposal, delta specs and `tasks.md` are reviewed in
48
+ a spec-only PR (`spec: <change-id>`) and approved by merging it (ADR 0011).
49
+ `run-tasks.sh` refuses changes that are not on `main`, and CI validates
50
+ spec PRs with `openspec validate --strict` instead of the gates. Only the
51
+ task loop implements: `/opsx:apply` is disabled (ADR 0013).
52
+ 2. **Before merge**: the PR is reviewed. CI re-runs the gates independently,
53
+ and the `main` ruleset requires it to pass.
@@ -0,0 +1,42 @@
1
+ # Stack
2
+
3
+ Derived from the reference repositories (see ADR 0007). Every product starts
4
+ from one of the app templates; both share the same harness, workflow and
5
+ standards, and differ only in the framework.
6
+
7
+ | Stack | Template | Use for | Details |
8
+ | -------- | -------------------------------- | -------------------------------------- | ------------------------ |
9
+ | `nextjs` | `data-matters-io/nextjs-new-app` | Web applications (UI + route handlers) | `stacks/nextjs/stack.md` |
10
+ | `nestjs` | `data-matters-io/nestjs-new-app` | Backend services (HTTP APIs) | `stacks/nestjs/stack.md` |
11
+
12
+ In a product repo, the chosen stack's files are in `.harness/standards/stack/`.
13
+
14
+ ## Shared by every stack
15
+
16
+ | Area | Choice |
17
+ | --------------- | ------------------------------------------------------------------------------------- |
18
+ | Language | TypeScript, `strict: true` |
19
+ | Package manager | pnpm 12.11.2, exact in `packageManager` + `devEngines` (ADR 0014); one lockfile |
20
+ | npm | Never in a project (`devEngines` makes it refuse); 12.2.0 on machines, user-level |
21
+ | Validation | Zod 4 |
22
+ | ORM / database | Prisma 7 (`@prisma/adapter-pg`) / PostgreSQL 16+ |
23
+ | Testing | Jest 30 + `ts-jest` |
24
+ | Lint / format | `@data-matters.io/eslint-config` 4.x (stack preset; brings `eslint` and `prettier`) |
25
+ | Git hooks | `data-matters-eslint-init` (postinstall). No husky. |
26
+ | Local infra | Podman (preferred) or Docker via Compose, driven by `make` |
27
+ | Spec workflow | OpenSpec (ADR 0002) + agent-harness |
28
+ | OpenSpec CLI | `@fission-ai/openspec` devDependency, exact pin (`.harness/openspec.sh`; ADR 0013) |
29
+ | CI | GitHub Actions: the template's `ci.yaml` plus `gates.yml` running `.harness/gates.sh` |
30
+ | CI on spec PRs | Jobs gated on `.harness/pr-scope.sh`: spec-only PRs run `openspec validate` only |
31
+ | Hosting | VPS, provisioned via `vps-bootstrap` |
32
+ | Secrets | Environment variables (or a secrets manager); never committed |
33
+
34
+ ## Stack presets
35
+
36
+ `templates/stacks/<stack>/` holds the gate commands (`gates.conf`) and the CI
37
+ workflow (`gates.yml`) validated against each template:
38
+
39
+ | Stack | Validated against |
40
+ | -------- | ---------------------------- |
41
+ | `nextjs` | see `stacks/nextjs/stack.md` |
42
+ | `nestjs` | see `stacks/nestjs/stack.md` |