@data-matters.io/agent-harness 0.1.8 → 0.1.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@data-matters.io/agent-harness",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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": {
@@ -35,6 +35,18 @@ harness_load_env() {
35
35
  HARNESS_CLAUDE=(env -u HARNESS_TELEGRAM_BOT_TOKEN -u HARNESS_TELEGRAM_CHAT_ID -u HARNESS_ANTHROPIC_API_KEY -u HARNESS_ANTHROPIC_BASE_URL claude)
36
36
 
37
37
  # ------------------------------------------- optional API key (ADR 0020) --
38
+ # harness_session_error_text <session.json>: the text of an errored session (its
39
+ # result and api_error), or the raw output when it is not JSON. Empty when the
40
+ # session did not fail.
41
+ harness_session_error_text() {
42
+ node -e "$harness_session_js"'
43
+ const raw = (() => { try { return require("fs").readFileSync(process.argv[1], "utf8"); } catch { return ""; } })();
44
+ const d = session(process.argv[1]);
45
+ process.stdout.write(Object.keys(d).length
46
+ ? (d.is_error ? [d.result, d.api_error].filter((x) => typeof x === "string").join(" ") : "")
47
+ : raw);' "$1" 2>/dev/null
48
+ }
49
+
38
50
  # harness_api_refused <session.json>: the session ended on an error that says
39
51
  # the API cannot serve it: no credit, a quota, a spend or usage limit, billing,
40
52
  # an invalid key, or the gateway itself failing (503 or another 5xx, a
@@ -43,12 +55,7 @@ HARNESS_CLAUDE=(env -u HARNESS_TELEGRAM_BOT_TOKEN -u HARNESS_TELEGRAM_CHAT_ID -u
43
55
  # a task about authentication or billing never matches by its own words.
44
56
  harness_api_refused() {
45
57
  local text
46
- text="$(node -e "$harness_session_js"'
47
- const raw = (() => { try { return require("fs").readFileSync(process.argv[1], "utf8"); } catch { return ""; } })();
48
- const d = session(process.argv[1]);
49
- process.stdout.write(Object.keys(d).length
50
- ? (d.is_error ? [d.result, d.api_error].filter((x) => typeof x === "string").join(" ") : "")
51
- : raw);' "$1" 2>/dev/null)" || return 1
58
+ text="$(harness_session_error_text "$1")" || return 1
52
59
  [[ -n "$text" ]] || return 1
53
60
  harness_api_unreachable "$text" && return 0
54
61
  harness_quota_transient "$text" && return 1
@@ -56,86 +63,30 @@ harness_api_refused() {
56
63
  grep -qiE "credit balance|spend(ing)? limit|billing|usage limit|rate limit|out of usage|invalid api key|authentication" <<<"$text"
57
64
  }
58
65
 
59
- # ------------------------------------------------ machine mode (ADR 0025) --
60
- # harness_mode: api, plan or blocked, from the machine's mode file
61
- # (HARNESS_MODE_FILE, default ~/.local/state/harness/mode; its only writer is
62
- # the watchdog). A missing, unreadable or unrecognised file is api, the
63
- # preferred mode.
64
- harness_mode() {
65
- local f="${HARNESS_MODE_FILE:-$HOME/.local/state/harness/mode}" line mode=api
66
- if [[ -r "$f" ]]; then
67
- while IFS= read -r line; do
68
- [[ "$line" == mode=* ]] && mode="${line#mode=}"
69
- done <"$f"
70
- fi
71
- case "$mode" in api|plan|blocked) echo "$mode" ;; *) echo api ;; esac
72
- }
73
-
74
- # harness_claude_session <timeout-seconds> <session.json> <claude args...>: runs
75
- # one headless Claude Code session (the prompt on stdin) and writes its output
76
- # (stdout and stderr) to <session.json>; returns the exit status of the run
77
- # that produced it (124: timed out). Without HARNESS_ANTHROPIC_API_KEY this is
78
- # the account login, exactly as before. With it, the session runs on the API
79
- # first (the key goes to that process only, through its environment, never its
80
- # arguments or a log); when the API refuses it (harness_api_refused), the SAME
81
- # attempt repeats on the plan login. Which one produced the output is written to
82
- # <session.json>.source (api|plan), which the usage ledger reads.
83
- # HARNESS_ANTHROPIC_BASE_URL is the optional base URL of that key (a gateway):
84
- # it reaches the API run only, as ANTHROPIC_BASE_URL, and the plan run never
85
- # sees it. A key from a gateway without its base URL would be sent to the
86
- # Anthropic API and refused.
87
- harness_claude_session() {
88
- local secs="$1" out="$2" key="${HARNESS_ANTHROPIC_API_KEY:-}" base="${HARNESS_ANTHROPIC_BASE_URL:-}" rc=0 prompt mode unset=()
89
- shift 2
90
- rm -f "$out.source"
91
- # The machine mode (ADR 0025) names the authentication to start on: api → the
92
- # key and its base URL, plan → the account login with both removed, blocked →
93
- # nothing runs. The fallback below stays as the net for a refusal that
94
- # happens after the mode was read.
95
- mode="$(harness_mode)"
96
- if [[ "$mode" == blocked ]]; then
97
- echo '{"type":"result","is_error":true,"api_error":"usage_limit_reached","result":"harness: the machine is blocked (the API does not respond and the account is out of usage); usage limit reached"}' >"$out"
98
- return 1
99
- fi
100
- if [[ -z "$key" || "$mode" == plan ]]; then
101
- # No key configured, or the API is down: the session is the account login,
102
- # on the Anthropic API. A base URL left in the environment would point that
103
- # login at a gateway, and a key must not reach it.
104
- [[ -z "$key" ]] || unset=(-u ANTHROPIC_API_KEY)
105
- [[ -z "$base" ]] || unset+=(-u ANTHROPIC_BASE_URL)
106
- if (( ${#unset[@]} )); then
107
- timeout "$secs" env "${unset[@]}" "${HARNESS_CLAUDE[@]}" "$@" >"$out" 2>&1 || rc=$?
108
- else
109
- timeout "$secs" "${HARNESS_CLAUDE[@]}" "$@" >"$out" 2>&1 || rc=$?
110
- fi
111
- [[ -z "$key" ]] || echo plan >"$out.source"
112
- return "$rc"
113
- fi
114
- prompt="$(mktemp)"
115
- cat >"$prompt"
116
- if [[ -n "$base" ]]; then
117
- ANTHROPIC_API_KEY="$key" ANTHROPIC_BASE_URL="$base" timeout "$secs" "${HARNESS_CLAUDE[@]}" "$@" <"$prompt" >"$out" 2>&1 || rc=$?
118
- else
119
- ANTHROPIC_API_KEY="$key" timeout "$secs" "${HARNESS_CLAUDE[@]}" "$@" <"$prompt" >"$out" 2>&1 || rc=$?
120
- fi
121
- # Whatever the session printed, the key itself never stays in the log.
122
- K="$key" node -e 'const fs = require("fs"); const f = process.argv[1];
123
- try { const s = fs.readFileSync(f, "utf8"); if (s.includes(process.env.K)) fs.writeFileSync(f, s.split(process.env.K).join("[redacted]")); } catch {}' "$out" 2>/dev/null || true
124
- if (( rc != 124 )) && harness_api_refused "$out"; then
125
- echo "harness: the API did not serve the session (refused, or the gateway failed); repeating it on the plan login" >&2
126
- rc=0
127
- if [[ -n "$base" ]]; then
128
- timeout "$secs" env -u ANTHROPIC_API_KEY -u ANTHROPIC_BASE_URL "${HARNESS_CLAUDE[@]}" "$@" <"$prompt" >"$out" 2>&1 || rc=$?
129
- else
130
- timeout "$secs" env -u ANTHROPIC_API_KEY "${HARNESS_CLAUDE[@]}" "$@" <"$prompt" >"$out" 2>&1 || rc=$?
131
- fi
132
- echo plan >"$out.source"
133
- else
66
+ # ----------------------------------- machine layer shim (ADR 0026, section 9) --
67
+ # The session wrapper and the machine mode live in the machine layer,
68
+ # machine/session.sh, in the VPS runtime (HARNESS_WATCH_RUNTIME, default
69
+ # ~/.local/share/agent-harness/watch). HARNESS_MACHINE_API=1 is the contract.
70
+ # Without a runtime (CI, a laptop, a clone elsewhere) a session is a plain claude
71
+ # with the caller's own environment and the mode is api: the degraded fallback of
72
+ # ADR 0025, section 8. A key from HARNESS_ANTHROPIC_API_KEY is not used then.
73
+ # shellcheck disable=SC2034 # the contract marker, read by the runtime
74
+ HARNESS_MACHINE_API=1
75
+ harness_machine_session_file() { printf '%s' "${HARNESS_WATCH_RUNTIME:-$HOME/.local/share/agent-harness/watch}/machine/session.sh"; }
76
+ if [[ -r "$(harness_machine_session_file)" ]]; then
77
+ # shellcheck source=/dev/null
78
+ . "$(harness_machine_session_file)"
79
+ else
80
+ harness_mode() { echo api; }
81
+ harness_claude_session() { # <timeout-seconds> <session.json> <claude args...>
82
+ local secs="$1" out="$2" rc=0
83
+ shift 2
84
+ rm -f "$out.source"
85
+ timeout "$secs" "${HARNESS_CLAUDE[@]}" "$@" >"$out" 2>&1 || rc=$?
134
86
  echo api >"$out.source"
135
- fi
136
- rm -f "$prompt"
137
- return "$rc"
138
- }
87
+ return "$rc"
88
+ }
89
+ fi
139
90
 
140
91
  # Worktree where the task loop runs a change (ADR 0012), outside the checkout so
141
92
  # lint, tests and type-checks of the main checkout never see it:
@@ -28,5 +28,6 @@ TASK_ci_fix_retry=R3 # failing checks after a failed fix (watcher, ADR 0019)
28
28
  TASK_review_comments=R2 # owner comments on implementation/sync PRs (ADR 0019, 1b)
29
29
  TASK_diagnosis=R3 # explaining an escalation
30
30
  TASK_review=R3 # PR review
31
+ TASK_issue_fix=R3 # a fix for an open harness-fault issue (ADR 0027, decision 5); unused until fix mode is enabled
31
32
  TASK_archive_fix=R1 # editorial split of a merged requirement over 500 characters (ADR 0022), gate-verified
32
33
  TASK_routine=R1 # hand edits around sync and archive
@@ -34,8 +34,11 @@ harness_quota_markers='limite de uso do seu plano|cota semanal|sera resetado em|
34
34
  # Transient markers, on normalized text: the request may be retried.
35
35
  harness_quota_transient_markers='o consumo da requisicao anterior esta sendo atualizado|tente novamente em alguns segundos|temporarily at capacity|repeated [0-9]+ overloaded errors'
36
36
 
37
- # Gateway failures, on normalized text: the API does not answer.
38
- harness_gateway_markers='api error:? \(?5[0-9]{2}|(status|error|http)[ :=]+5[0-9]{2}\b|"api_error_status": ?5[0-9]{2}|\b50[0-9] (service unavailable|bad gateway|gateway time-?out)|service unavailable|bad gateway|gateway time-?out|econnrefused|econnreset|etimedout|enotfound|eai_again|socket hang up|fetch failed|unable to connect|connection error|connection refused|request timed out'
37
+ # Gateway failures, on normalized text: the API does not answer. This is the
38
+ # one list for the whole system (ADR 0026, section 6): the machine library
39
+ # (vps-bootstrap) loads this file and keeps a copy only as a degraded fallback,
40
+ # and a test there fails if the two disagree.
41
+ harness_gateway_markers='api error:? *\(?5[0-9]{2}|(status|error|http)[ :=]+5[0-9]{2}\b|"api_error_status": ?5[0-9]{2}|\b50[0-9] (service unavailable|bad gateway|gateway time-?out)|service unavailable|bad gateway|gateway time-?out|overloaded|upstream|econnrefused|econnreset|etimedout|enotfound|eai_again|socket hang up|fetch failed|unable to connect|connection (error|refused|reset|closed)|timed? ?out|could not resolve|network error|context validation'
39
42
 
40
43
  # harness_quota_transient <text>: a retryable condition, not a quota.
41
44
  harness_quota_transient() {
@@ -54,3 +57,14 @@ harness_quota_exhausted() {
54
57
  harness_api_unreachable() {
55
58
  grep -qE -- "$harness_gateway_markers" <<<"$(harness_quota_normalize "$1")"
56
59
  }
60
+
61
+ # harness_api_fault_class <text>: how a session error counts for the machine
62
+ # mode (ADR 0025 section 8, ADR 0026 sections 3 and 6). Prints one word:
63
+ # transient a passing failure: counts only after several in a row
64
+ # down the API does not serve: a quota, or a gateway failure
65
+ # none not a failure of the API
66
+ harness_api_fault_class() {
67
+ if harness_quota_transient "$1"; then printf 'transient'; return 0; fi
68
+ if harness_quota_exhausted "$1" || harness_api_unreachable "$1"; then printf 'down'; return 0; fi
69
+ printf 'none'
70
+ }