@data-matters.io/agent-harness 0.1.2 → 0.1.4
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/package.json +2 -2
- package/templates/.claude/settings.json +1 -0
- package/templates/.harness/app-smoke.sh +107 -0
- package/templates/.harness/gates.conf.example +9 -0
- package/templates/.harness/gates.sh +27 -4
- package/templates/.harness/lib.sh +4 -2
- package/templates/.harness/models.conf +7 -6
- package/templates/.harness/run-tasks.sh +81 -1
- package/templates/stacks/nextjs/gates.conf +14 -0
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@data-matters.io/agent-harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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/
|
|
8
|
+
"url": "git+https://github.com/data-matters-io/agent-harness.git"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
11
|
"harness": "bin/harness"
|
|
@@ -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
|
-
#
|
|
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 ]]
|
|
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
|
|
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
|
|
@@ -6,20 +6,21 @@
|
|
|
6
6
|
#
|
|
7
7
|
# Overrides, applied in this order after this file:
|
|
8
8
|
# .harness/models.local.conf per project (project-owned, never synced)
|
|
9
|
-
# $HARNESS_MODELS_FILE per machine (e.g.
|
|
9
|
+
# $HARNESS_MODELS_FILE per machine (e.g. a cheaper R3: R3="--model opus --effort low")
|
|
10
10
|
#
|
|
11
11
|
# Rollout (decision 7): with HARNESS_MODEL_ROUTING=measure (the default) the
|
|
12
12
|
# levels are computed and recorded but no flags are passed, so sessions keep
|
|
13
13
|
# the account's default model; set HARNESS_MODEL_ROUTING=on (env file) to switch.
|
|
14
14
|
# shellcheck shell=bash disable=SC2034
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# No level uses Fable (amendment 2026-10-31 to ADR 0020).
|
|
17
|
+
R4="--model opus --effort high" # reserved: large product documents, last rung of the ladder
|
|
18
|
+
R3="--model opus --effort medium" # design/hard: specs, ambiguous work, unclear diagnosis
|
|
18
19
|
R2="--model sonnet --effort medium" # build: a well-specified, gate-verified task
|
|
19
20
|
R1="--model haiku --effort medium" # routine: a mechanical, local edit
|
|
20
21
|
|
|
21
|
-
TASK_spec_author=
|
|
22
|
-
TASK_spec_adjust=
|
|
22
|
+
TASK_spec_author=R3 # /opsx:propose in an interactive or channel session; R4 for big product documents
|
|
23
|
+
TASK_spec_adjust=R2 # spec PR review comments (watcher, ADR 0017): bounded edits to an existing spec
|
|
23
24
|
TASK_impl=R2 # task loop (run-tasks.sh); tasks.md may override
|
|
24
25
|
TASK_conflict=R3 # merge conflicts on a PR (watcher, ADR 0019)
|
|
25
26
|
TASK_ci_fix=R2 # failing checks, clear cause (watcher, ADR 0019)
|
|
@@ -27,5 +28,5 @@ TASK_ci_fix_retry=R3 # failing checks after a failed fix (watcher, ADR 0019)
|
|
|
27
28
|
TASK_review_comments=R2 # owner comments on implementation/sync PRs (ADR 0019, 1b)
|
|
28
29
|
TASK_diagnosis=R3 # explaining an escalation
|
|
29
30
|
TASK_review=R3 # PR review
|
|
30
|
-
TASK_archive_fix=
|
|
31
|
+
TASK_archive_fix=R1 # editorial split of a merged requirement over 500 characters (ADR 0022), gate-verified
|
|
31
32
|
TASK_routine=R1 # hand edits around sync and archive
|
|
@@ -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,
|
|
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
|
+
)
|