@data-matters.io/agent-harness 0.1.1 → 0.1.3

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.
package/README.md CHANGED
@@ -18,7 +18,7 @@ minimumReleaseAgeExclude:
18
18
 
19
19
  ```sh
20
20
  pnpm add -D -E @data-matters.io/agent-harness
21
- pnpm exec harness init --new --stack nextjs # or nestjs; --existing for a codebase without tests
21
+ pnpm exec harness init --new --stack nextjs # or nestjs, vite; --existing for a codebase without tests
22
22
  ```
23
23
 
24
24
  ```json
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@data-matters.io/agent-harness",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Spec-Driven Development with Claude Code: gates, task loop and standards installed into a product repository",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/data-matters-io/data-matters-agent-harness.git"
8
+ "url": "git+https://github.com/data-matters-io/agent-harness.git"
9
9
  },
10
10
  "bin": {
11
11
  "harness": "bin/harness"
@@ -8,6 +8,7 @@ standards, and differ only in the framework.
8
8
  | -------- | -------------------------------- | -------------------------------------- | ------------------------ |
9
9
  | `nextjs` | `data-matters-io/nextjs-new-app` | Web applications (UI + route handlers) | `stacks/nextjs/stack.md` |
10
10
  | `nestjs` | `data-matters-io/nestjs-new-app` | Backend services (HTTP APIs) | `stacks/nestjs/stack.md` |
11
+ | `vite` | none (ADR 0023) | Static Vite + TypeScript sites, no DB | `stacks/vite/stack.md` |
11
12
 
12
13
  In a product repo, the chosen stack's files are in `.harness/standards/stack/`.
13
14
 
@@ -40,3 +41,4 @@ workflow (`gates.yml`) validated against each template:
40
41
  | -------- | ---------------------------- |
41
42
  | `nextjs` | see `stacks/nextjs/stack.md` |
42
43
  | `nestjs` | see `stacks/nestjs/stack.md` |
44
+ | `vite` | see `stacks/vite/stack.md` |
@@ -0,0 +1,10 @@
1
+ # Architecture: Vite static sites
2
+
3
+ A static site: everything runs in the browser and `vite build` emits `dist/`.
4
+ Deviations need an ADR in the product repo.
5
+
6
+ - Source in `src/`, entry from `index.html`; tests beside the code.
7
+ - No server, no database, no secrets: nothing in `src/` or `dist/` may hold one.
8
+ - Keep pure logic (data, rules) in modules apart from DOM code, so it is
9
+ testable without a browser.
10
+ - Dependencies are added only when the spec declares them (ADR 0015).
@@ -0,0 +1,13 @@
1
+ # Conventions: Vite static sites
2
+
3
+ Extends the shared `conventions.md`.
4
+
5
+ - Type-only imports inline: `import { type Foo } from '…'`.
6
+ - Import order is auto-fixed by lint (`@data-matters.io/eslint-config`).
7
+ - Files are kebab-case `.ts`; tests are `<file>.test.ts` beside the code.
8
+ - Env vars exposed to the client use the `VITE_` prefix and are never secret.
9
+
10
+ ## Local workflow
11
+
12
+ `pnpm dev`, `pnpm build`, `pnpm test`, `pnpm lint`, `pnpm typecheck` and
13
+ `pnpm gates` (all gates).
@@ -0,0 +1,27 @@
1
+ # Stack: Vite static sites
2
+
3
+ No app template: the preset is derived from `data-matters-io/play-around-world`
4
+ (ADR 0023), with `@data-matters.io/eslint-config` 4.0.0.
5
+
6
+ | Area | Choice | Version |
7
+ | ------------ | ----------------------------------------- | ------- |
8
+ | Runtime | Node.js (build and tooling only) | 24.x |
9
+ | Bundler | Vite, static output in `dist/` | 8.x |
10
+ | Language | TypeScript, `strict: true` | 5.9.x |
11
+ | Testing | Vitest + jsdom, coverage with istanbul | 5.x |
12
+ | Lint preset | `typescript` | 4.x |
13
+ | Backend / DB | None: no server, no Prisma, no PostgreSQL | — |
14
+
15
+ ## Gates (`templates/stacks/vite/gates.conf`)
16
+
17
+ | Gate | Command |
18
+ | --------- | ------------------------------------------------------------------------------ |
19
+ | setup | none |
20
+ | test | `vitest run` with istanbul (floor: 85% lines, statements, branches, functions) |
21
+ | lint | `eslint . --max-warnings 0` |
22
+ | typecheck | `tsc --noEmit` |
23
+ | build | `vite build` |
24
+ | db | none (no database) |
25
+
26
+ The coverage floor lives in `gates.conf`, not in `vite.config.ts`, so an agent
27
+ cannot lower it.
@@ -0,0 +1,25 @@
1
+ # Testing: Vite static sites
2
+
3
+ Extends the shared `testing.md`.
4
+
5
+ Tooling: Vitest 5 with jsdom, coverage provider istanbul
6
+ (`@vitest/coverage-istanbul`).
7
+
8
+ ## Tiers
9
+
10
+ | Tier | Pattern | Status |
11
+ | ----------- | ------------------------------ | ------------------- |
12
+ | Unit | `src/**/*.test.ts` beside code | in use; `test` gate |
13
+ | Integration | none: there is no backend | not applicable |
14
+ | E2E | none yet | not a harness gate |
15
+
16
+ ## Rules
17
+
18
+ - Unit tests do not touch the network.
19
+ - DOM code is tested through jsdom with accessible queries, not implementation
20
+ details.
21
+
22
+ ## Coverage floor
23
+
24
+ 85% on lines, statements, branches and functions, over every file matching
25
+ `src/**/*.ts` (set in `gates.conf`).
@@ -43,6 +43,7 @@
43
43
  "Edit(./.harness/pr-scope.sh)",
44
44
  "Edit(./.harness/deps.sh)",
45
45
  "Edit(./.harness/db-smoke.sh)",
46
+ "Edit(./.harness/app-smoke.sh)",
46
47
  "Edit(./.harness/models.conf)",
47
48
  "Edit(./.harness/models.local.conf)",
48
49
  "Edit(./.harness/model-flags.sh)",
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ # Application smoke gate (ADR 0024): builds the app, starts it against a migrated
3
+ # and seeded database, runs the project's end-to-end smoke test, stops the app.
4
+ # Meant for GATE_FINAL in gates.conf: it is slow (build + server + database), so
5
+ # it runs once at the end of a change, not on every task or in the Stop hook.
6
+ #
7
+ # Usage: .harness/app-smoke.sh --url <url> --start <cmd> --test <cmd> [options]
8
+ # --url <url> where the app answers; any HTTP response counts as up
9
+ # --start <cmd> starts the server (runs in the background, in its own process group)
10
+ # --test <cmd> the smoke test; its exit code is the gate's
11
+ # --build <cmd> run first (e.g. the production build)
12
+ # --migrate <cmd> --seed <cmd>
13
+ # prepare a throwaway database through .harness/db-smoke.sh
14
+ # (ADR 0016); the commands get DATABASE_URL. Without them the
15
+ # app runs against whatever the environment configures
16
+ # --if-exists <f> pass without running anything when <f> does not exist, so a
17
+ # preset can carry the gate for projects that may not have the test
18
+ # --wait <seconds> how long to wait for the app (default 120)
19
+ #
20
+ # Example (gates.conf, nextjs):
21
+ # GATE_FINAL=("smoke:.harness/app-smoke.sh --if-exists scripts/smoke-test.ts \
22
+ # --migrate '$bin/prisma migrate deploy' --seed '$bin/prisma db seed' \
23
+ # --build '$bin/next build' --start '$bin/next start -p 3100' \
24
+ # --url http://127.0.0.1:3100 --test '$bin/tsx scripts/smoke-test.ts'")
25
+ #
26
+ # The server log goes to .harness/state/gates/app-smoke-server.log and its tail is
27
+ # printed when the app does not come up or the test fails. A machine that cannot
28
+ # run it skips the gate with HARNESS_SKIP_GATES=smoke (CI still can).
29
+ set -euo pipefail
30
+
31
+ usage() { sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; }
32
+ die() { echo "app-smoke: $*" >&2; exit 1; }
33
+
34
+ url="" start="" test_cmd="" build="" migrate="" seed="" if_exists="" wait_s=120 inner=0
35
+ while [[ $# -gt 0 ]]; do
36
+ case "$1" in
37
+ --url) url="${2:?}"; shift 2 ;;
38
+ --start) start="${2:?}"; shift 2 ;;
39
+ --test) test_cmd="${2:?}"; shift 2 ;;
40
+ --build) build="${2:?}"; shift 2 ;;
41
+ --migrate) migrate="${2:?}"; shift 2 ;;
42
+ --seed) seed="${2:?}"; shift 2 ;;
43
+ --if-exists) if_exists="${2:?}"; shift 2 ;;
44
+ --wait) wait_s="${2:?}"; shift 2 ;;
45
+ --inner) inner=1; shift ;;
46
+ -h|--help) usage 0 ;;
47
+ *) die "unknown option: $1" ;;
48
+ esac
49
+ done
50
+ [[ -n "$url" && -n "$start" && -n "$test_cmd" ]] || usage 1
51
+ [[ "$wait_s" =~ ^[0-9]+$ ]] || die "--wait needs a number of seconds"
52
+ [[ "$url" =~ ^https?://([^/:]+):([0-9]+)(/.*)?$ ]] || die "--url needs http(s)://host:port"
53
+ host="${BASH_REMATCH[1]}" port="${BASH_REMATCH[2]}"
54
+
55
+ if [[ -n "$if_exists" && ! -e "$if_exists" ]]; then
56
+ echo "app-smoke: no $if_exists in this project; nothing to run"
57
+ exit 0
58
+ fi
59
+
60
+ # With a database to prepare, hand over to db-smoke.sh, which keeps its container
61
+ # alive while this script runs again (--inner) as its check command.
62
+ if [[ $inner -eq 0 && ( -n "$migrate" || -n "$seed" ) ]]; then
63
+ [[ -n "$migrate" && -n "$seed" ]] || die "--migrate and --seed go together"
64
+ inner_cmd="$(printf '%q ' "$0" --inner --url "$url" --start "$start" --test "$test_cmd" --wait "$wait_s")"
65
+ [[ -z "$build" ]] || inner_cmd+="$(printf -- "--build %q" "$build")"
66
+ exec "$(dirname "$0")/db-smoke.sh" "$migrate" "$seed" "$inner_cmd"
67
+ fi
68
+
69
+ log_dir="${HARNESS_STATE_DIR:-.harness/state}/gates"
70
+ mkdir -p "$log_dir"
71
+ server_log="$log_dir/app-smoke-server.log"
72
+ server_pid=""
73
+ stop_server() {
74
+ [[ -n "$server_pid" ]] || return 0
75
+ kill -TERM -- "-$server_pid" 2>/dev/null || true
76
+ sleep 1
77
+ kill -KILL -- "-$server_pid" 2>/dev/null || true
78
+ }
79
+ trap stop_server EXIT
80
+ show_server_log() { echo "----- last 40 lines of the server log -----"; tail -n 40 "$server_log" 2>/dev/null || true; echo "-----"; }
81
+
82
+ export PATH="$PWD/node_modules/.bin:$PATH"
83
+ if [[ -n "$build" ]]; then
84
+ echo "app-smoke: build: $build"
85
+ bash -c "$build" || die "build failed"
86
+ fi
87
+
88
+ # The port must be free: a leftover server would answer instead of this build.
89
+ if (exec 3<>"/dev/tcp/$host/$port") 2>/dev/null; then die "$host:$port is already in use"; fi
90
+ echo "app-smoke: start: $start"
91
+ setsid bash -c "$start" >"$server_log" 2>&1 &
92
+ server_pid=$!
93
+
94
+ up=0
95
+ for (( i = 0; i < wait_s; i++ )); do
96
+ kill -0 "$server_pid" 2>/dev/null || { show_server_log; die "the app exited before answering on $url"; }
97
+ if (exec 3<>"/dev/tcp/$host/$port") 2>/dev/null; then up=1; break; fi
98
+ sleep 1
99
+ done
100
+ [[ $up -eq 1 ]] || { show_server_log; die "the app did not answer on $url within ${wait_s}s"; }
101
+
102
+ echo "app-smoke: test: $test_cmd"
103
+ if ! BASE_URL="${url%/}" bash -c "$test_cmd"; then
104
+ show_server_log
105
+ die "the smoke test failed"
106
+ fi
107
+ echo "app-smoke: passed"
@@ -29,3 +29,12 @@ GATE_EXTRA=(
29
29
  # "coverage:npm run test:coverage -- --coverage.thresholds.lines=80"
30
30
  # "security:npm audit --audit-level=high"
31
31
  )
32
+
33
+ # Optional final gates (ADR 0024): slow end-to-end checks ("name:command"). The task
34
+ # loop runs them once, after the last task and before the change's PR opens; a
35
+ # failure gets repair sessions. Never in a default run, the Stop hook or per task.
36
+ # `.harness/gates.sh --final` (or `gates.sh <name>`) runs them by hand or in CI.
37
+ # .harness/app-smoke.sh builds, starts the app on a seeded database and runs a test.
38
+ GATE_FINAL=(
39
+ # "smoke:.harness/app-smoke.sh --if-exists scripts/smoke-test.ts --migrate '...' --seed '...' --build '...' --start '...' --url http://127.0.0.1:3100 --test '...'"
40
+ )
@@ -2,10 +2,14 @@
2
2
  # Runs every Definition-of-Done gate declared in .harness/gates.conf.
3
3
  # Exit 0 only if all gates pass. Used by the Stop hook, the task loop and CI.
4
4
  #
5
- # Usage: .harness/gates.sh [--stop] [gate ...]
6
- # no args run every gate (task loop, CI)
5
+ # Usage: .harness/gates.sh [--stop|--final] [gate ...]
6
+ # no args run every gate but the final ones (task loop, CI)
7
7
  # --stop run only STOP_GATES from gates.conf (fast profile for the Stop hook)
8
- # gate ... run only the named gates
8
+ # --final run only GATE_FINAL from gates.conf: slow end-to-end checks that run
9
+ # once, at the end of a change in the task loop, before its PR opens
10
+ # (ADR 0024); `gates.sh --final` runs them in CI or by hand
11
+ # --has-final exit 0 when GATE_FINAL is not empty, 1 otherwise (runs nothing)
12
+ # gate ... run only the named gates (final ones included)
9
13
  #
10
14
  # HARNESS_SKIP_GATES="name ..." skips the named gates on this machine (e.g.
11
15
  # integration where no container runtime exists). Skips are printed on every
@@ -25,12 +29,22 @@ REQUIRED_GATES=(test lint typecheck build)
25
29
  GATE_EXTRA=()
26
30
  GATE_SETUP=""
27
31
  STOP_GATES=()
32
+ GATE_FINAL=()
28
33
  # shellcheck source=/dev/null
29
34
  source "$conf"
30
35
 
31
36
  selected=("$@")
32
37
  if [[ "${1-}" == "--stop" ]]; then
33
38
  selected=("${STOP_GATES[@]}")
39
+ elif [[ "${1-}" == "--has-final" ]]; then
40
+ [[ ${#GATE_FINAL[@]} -gt 0 ]]; exit
41
+ elif [[ "${1-}" == "--final" ]]; then
42
+ selected=()
43
+ for entry in "${GATE_FINAL[@]}"; do selected+=("${entry%%:*}"); done
44
+ if [[ ${#selected[@]} -eq 0 ]]; then
45
+ echo "gates: no final gates configured (GATE_FINAL)"
46
+ exit 0
47
+ fi
34
48
  fi
35
49
 
36
50
  log_dir="$root/.harness/state/gates"
@@ -49,9 +63,18 @@ for entry in "${GATE_EXTRA[@]}"; do
49
63
  names+=("${entry%%:*}")
50
64
  cmds+=("${entry#*:}")
51
65
  done
66
+ # Final gates run only on request (--final or by name), never in a default run.
67
+ final_names=" "
68
+ for entry in "${GATE_FINAL[@]}"; do
69
+ names+=("${entry%%:*}")
70
+ cmds+=("${entry#*:}")
71
+ final_names+="${entry%%:*} "
72
+ done
52
73
 
53
74
  is_selected() {
54
- [[ ${#selected[@]} -eq 0 ]] && return 0
75
+ if [[ ${#selected[@]} -eq 0 ]]; then
76
+ [[ "$final_names" != *" $1 "* ]]; return
77
+ fi
55
78
  local s
56
79
  for s in "${selected[@]}"; do [[ "$s" == "$1" ]] && return 0; done
57
80
  return 1
@@ -93,9 +93,11 @@ harness_claude_flags() {
93
93
 
94
94
  # harness_usage_error <session.json>: the session ended on an account usage
95
95
  # limit or on missing usage credits ("You're out of usage credits. Switch to
96
- # another model..."), not on the work.
96
+ # another model..."), not on the work. Claude Code names the window in the
97
+ # message ("You've hit your 5-hour limit", "...your weekly limit", "...your
98
+ # session limit") and sets api_error to usage_limit_reached, so both are matched.
97
99
  harness_usage_error() {
98
- grep -qiE "hit your (session|usage) limit|usage limit reached|out of usage credits|credit balance is too low" "$1" 2>/dev/null
100
+ grep -qiE "hit your [^\"]{0,40}limit|usage_limit_reached|usage limit reached|out of usage credits|credit balance is too low" "$1" 2>/dev/null
99
101
  }
100
102
 
101
103
  # harness_level_descend <level>: the level one below, for the fallback after a
@@ -16,6 +16,10 @@
16
16
  # one level down (R4 → R3 → R2 → R1) instead of pausing; the summary records
17
17
  # that session as "fallback" and the next at the level it ran. Pausing (exit 3)
18
18
  # is kept at R1 and with routing off.
19
+ # After the last task, the final gates (GATE_FINAL in gates.conf, ADR 0024:
20
+ # slow end-to-end checks such as the app smoke test) run once, before the PR is
21
+ # opened; a failure gets up to HARNESS_FINAL_ATTEMPTS repair sessions with the
22
+ # output as feedback, then escalates. A change with no GATE_FINAL skips the step.
19
23
  # Before the first task it installs the dependencies the spec declares
20
24
  # (.harness/deps.sh, ADR 0015) in their own commit; an agent that changes
21
25
  # dependency files escalates the run.
@@ -47,13 +51,14 @@
47
51
  # dependencies from the lockfile); HARNESS_MAIN_CHECKOUT
48
52
  # points at the main checkout, e.g. to copy a .env
49
53
  # HARNESS_MAX_ATTEMPTS attempts per task (default 4: the level ladder, ADR 0020)
54
+ # HARNESS_FINAL_ATTEMPTS repair sessions for failing final gates (default 2)
50
55
  # HARNESS_TASK_TIMEOUT max seconds per agent session (default 1800)
51
56
  # HARNESS_PERMISSION_MODE claude --permission-mode (default acceptEdits)
52
57
  # HARNESS_CLAUDE_ARGS extra args for claude (experiments; models come from models.conf)
53
58
  # HARNESS_COMMIT_TYPE conventional commit type (default feat)
54
59
  set -euo pipefail
55
60
 
56
- usage() { sed -n '2,50p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; }
61
+ usage() { sed -n '2,54p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; }
57
62
  die() { echo "run-tasks: $*" >&2; exit 1; }
58
63
 
59
64
  [[ $# -ge 1 ]] || usage 1
@@ -436,6 +441,81 @@ while line="$(next_task)"; [[ -n "$line" ]]; do
436
441
  fi
437
442
  done
438
443
 
444
+ # ---- final gates (ADR 0024): once, after the last task, before the PR opens.
445
+ # They are slow (build, server, database), so they are not part of every task's
446
+ # gates. A failure is repaired like a task: the agent gets the output, the loop
447
+ # re-runs the gates. HARNESS_SKIP_GATES skips them like any other gate.
448
+ final_attempts="${HARNESS_FINAL_ATTEMPTS:-2}"
449
+ final_gate_out=""
450
+ if [[ -x .harness/gates.sh ]] && .harness/gates.sh --has-final; then
451
+ echo "==> final gates"
452
+ final_ok=0
453
+ for (( attempt = 0; attempt <= final_attempts; attempt++ )); do
454
+ if (( attempt > 0 )); then
455
+ level="$(harness_task_level impl "")" || level="R2"
456
+ level="$(harness_level_climb "$level" "$attempt")"
457
+ level_flags="$(harness_level_flags "$level")"
458
+ cur_task_id="final"; cur_task="final gates"; cur_attempt="$attempt"; attempt_started="$(date +%s)"
459
+ write_state
460
+ echo " repair $attempt/$final_attempts at $level${level_flags:+ ($level_flags)}"
461
+ start=$(date +%s)
462
+ session_json="$log_dir/task-final-attempt-$attempt.json"
463
+ # shellcheck disable=SC2086
464
+ build_prompt "make the final gates pass: .harness/gates.sh --final (end-to-end checks run once at the end of the change). Fix the code or tests the change touched; the approved spec is the reference." "$final_gate_out" \
465
+ | timeout "$timeout_s" "${HARNESS_CLAUDE[@]}" -p --output-format json --permission-mode "$permission_mode" \
466
+ --disallowedTools "Bash(git commit:*)" "Bash(git push:*)" "Bash(.harness/spec-pr.sh:*)" \
467
+ "Bash(pnpm add:*)" "Bash(pnpm remove:*)" "Bash(pnpm update:*)" "Bash(npm install:*)" \
468
+ ${level_flags} ${HARNESS_CLAUDE_ARGS:-} >"$session_json" 2>&1 || true
469
+ warn_if_untrusted "$session_json"
470
+ if harness_usage_error "$session_json"; then
471
+ record final "$attempt" "$(( $(date +%s) - start ))" paused
472
+ run_status="paused"
473
+ git checkout -- . && git clean -fdq
474
+ echo "run-tasks: the agent session hit the account usage limit; pausing." >&2
475
+ echo " The final gates are not done; re-run after the limit resets." >&2
476
+ exit 3
477
+ fi
478
+ git checkout -- "$change_dir"
479
+ git clean -fdq -- "$change_dir"
480
+ if [[ -n "$(git status --porcelain -- "${dep_files[@]}")" ]]; then
481
+ record final "$attempt" "$(( $(date +%s) - start ))" deps
482
+ run_status="escalated"
483
+ echo "run-tasks: the final-gates repair changed dependency files; undeclared dependencies need a spec change." >&2
484
+ exit 2
485
+ fi
486
+ fi
487
+ # A repair must keep every other gate green too.
488
+ if (( attempt > 0 )) && ! gate_out="$(.harness/gates.sh 2>&1)"; then
489
+ record final "$attempt" "$(( $(date +%s) - start ))" fail
490
+ final_gate_out="$(tail -n 120 <<<"$gate_out")"
491
+ echo " FAIL (the repair broke the regular gates)"
492
+ continue
493
+ fi
494
+ if final_gate_out="$(.harness/gates.sh --final 2>&1)"; then
495
+ (( attempt == 0 )) || record final "$attempt" "$(( $(date +%s) - start ))" pass
496
+ final_ok=1
497
+ break
498
+ fi
499
+ printf '%s\n' "$final_gate_out" >"$log_dir/final-gates-attempt-$attempt.log"
500
+ (( attempt == 0 )) || record final "$attempt" "$(( $(date +%s) - start ))" fail
501
+ final_gate_out="$(tail -n 120 <<<"$final_gate_out")"
502
+ echo " FAIL (final gates)"
503
+ done
504
+ if [[ $final_ok -eq 0 ]]; then
505
+ run_status="escalated"
506
+ echo "run-tasks: the final gates still fail after $final_attempts repair attempts; escalating." >&2
507
+ echo " Work in progress is left on branch $branch in $wt. Logs: $log_dir/final-gates-attempt-*.log" >&2
508
+ exit 2
509
+ fi
510
+ if [[ -n "$(git status --porcelain)" ]]; then
511
+ git add -A
512
+ printf 'fix(%s): make the final gates pass\n\nRepaired by the task loop after .harness/gates.sh --final failed:\n\n%s\n' \
513
+ "$change" "$(head -n 20 "$log_dir/final-gates-attempt-0.log" 2>/dev/null)" | fold -s -w 72 | git commit -q -F -
514
+ echo " final gates repaired → committed"
515
+ fi
516
+ echo " final gates PASS"
517
+ fi
518
+
439
519
  run_status="done"
440
520
  cur_task_id=""; cur_task=""; cur_attempt=0; attempt_started=""
441
521
  echo "run-tasks: all tasks in $tasks are done."
@@ -24,6 +24,10 @@ GATE_BUILD="$bin/next build"
24
24
  # Formats openspec/ after an archive (the archive output is not formatted).
25
25
  FORMAT_SPECS="$bin/prettier --write openspec"
26
26
 
27
+ # Placeholder environment for the smoke test's build and server: no real login or
28
+ # external service is involved (the test signs its own sessions with the secret).
29
+ smoke_env="NEXTAUTH_URL=http://127.0.0.1:3100 NEXTAUTH_SECRET=harness-smoke-secret-not-used-anywhere-else GOOGLE_CLIENT_ID=smoke.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=smoke NEXT_TELEMETRY_DISABLED=1"
30
+
27
31
  # Stop hook runs the fast gates (~40s); build (~60s) runs in the loop and CI.
28
32
  STOP_GATES=(lint typecheck test)
29
33
 
@@ -34,3 +38,13 @@ GATE_EXTRA=(
34
38
  "db:.harness/db-smoke.sh '$bin/prisma migrate deploy' '$bin/prisma db seed'"
35
39
  # "security:pnpm audit --audit-level high"
36
40
  )
41
+
42
+ # Final gates (ADR 0024): slow end-to-end checks. The task loop runs them once, at
43
+ # the end of a change, before its PR opens (not per task, not in the Stop hook);
44
+ # `.harness/gates.sh --final` runs them by hand. The end-to-end smoke test needs a
45
+ # migrated and seeded database and a running app; the gate passes by itself in a
46
+ # project without scripts/smoke-test.ts. HARNESS_SKIP_GATES=smoke on a machine
47
+ # without a container runtime; CI runs it in its own e2e job.
48
+ GATE_FINAL=(
49
+ "smoke:.harness/app-smoke.sh --if-exists scripts/smoke-test.ts --migrate '$bin/prisma migrate deploy' --seed '$bin/prisma db seed' --build 'env $smoke_env $bin/next build' --start 'env $smoke_env $bin/next start -p 3100' --url http://127.0.0.1:3100 --test 'env $smoke_env $bin/tsx scripts/smoke-test.ts'"
50
+ )
@@ -0,0 +1,31 @@
1
+ # Gate commands for a static Vite + TypeScript site (no database, no server).
2
+ # Sourced by .harness/gates.sh from the repo root. Validated against
3
+ # data-matters-io/play-around-world: vite 8.3.5, typescript 5.9.3, vitest 5.0.5,
4
+ # @vitest/coverage-istanbul 5.0.5, jsdom 30.1.3, @data-matters.io/eslint-config 4.0.0.
5
+
6
+ # Gates call node_modules/.bin directly instead of `pnpm exec`: a gate must never
7
+ # install or mutate anything (same rule as the other stack presets). Dependencies
8
+ # are installed beforehand (`pnpm install --frozen-lockfile`).
9
+ bin="node_modules/.bin"
10
+
11
+ # No setup step: nothing to generate before the gates run.
12
+ GATE_SETUP=""
13
+
14
+ # Coverage floor 85% on every metric, statement-based (istanbul), measured over
15
+ # every source file (standards/testing.md). Kept here, not in vite.config.ts, so
16
+ # an agent cannot lower it. Raise it as coverage grows; never lower it.
17
+ GATE_TEST="$bin/vitest run --coverage.enabled --coverage.provider=istanbul --coverage.include='src/**/*.ts' --coverage.thresholds.lines=85 --coverage.thresholds.statements=85 --coverage.thresholds.branches=85 --coverage.thresholds.functions=85"
18
+ # Read-only and zero warnings (@data-matters.io/eslint-config, typescript preset).
19
+ GATE_LINT="$bin/eslint . --max-warnings 0"
20
+ GATE_TYPECHECK="$bin/tsc --noEmit"
21
+ GATE_BUILD="$bin/vite build"
22
+
23
+ # Formats openspec/ after an archive (the archive output is not formatted).
24
+ FORMAT_SPECS="$bin/prettier --write openspec"
25
+
26
+ # Stop hook runs the fast gates; build runs in the loop and CI.
27
+ STOP_GATES=(lint typecheck test)
28
+
29
+ GATE_EXTRA=(
30
+ # "security:pnpm audit --audit-level high"
31
+ )
@@ -0,0 +1,50 @@
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
+ # Reads the exact pnpm version from package.json#packageManager (ADR 0014).
19
+ # pnpm 12 needs v6.1.0+; the floating @v6 tag is older.
20
+ - uses: pnpm/action-setup@v6.1.0
21
+
22
+ - uses: actions/setup-node@v4
23
+ with:
24
+ node-version: 24
25
+ cache: pnpm
26
+
27
+ - name: Install dependencies
28
+ run: pnpm install --frozen-lockfile
29
+
30
+ # A harness installed from the package (ADR 0021) is generated by the
31
+ # install's "prepare"; a missing or stale one fails here.
32
+ - name: Check the harness
33
+ run: if [[ -x node_modules/.bin/harness ]]; then node_modules/.bin/harness check; fi
34
+
35
+ # After the install: with the package, .harness/ is generated by it.
36
+ # Spec-only PRs (ADR 0011, 0013) validate the specs instead of running the
37
+ # DoD gates; pr-scope.sh also fails a spec/<id> PR that changes anything
38
+ # outside openspec/changes/<id>/. Pushes to main always run the gates.
39
+ - name: Classify pull request
40
+ id: scope
41
+ run: .harness/pr-scope.sh >>"$GITHUB_OUTPUT"
42
+
43
+ # OpenSpec is a pinned devDependency (.harness/openspec.sh).
44
+ - name: Validate OpenSpec changes and specs
45
+ if: steps.scope.outputs.spec_only == 'true'
46
+ run: .harness/openspec.sh validate --all --strict --no-interactive
47
+
48
+ - name: Run DoD gates
49
+ if: steps.scope.outputs.spec_only != 'true'
50
+ run: .harness/gates.sh