@data-matters.io/agent-harness 0.1.8 → 0.1.10
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
|
@@ -35,6 +35,27 @@ 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
|
+
|
|
50
|
+
# harness_session_api_error <session.json>: the session's api_error field, the
|
|
51
|
+
# structured value the runtime sets when the API refused (usage_limit_reached).
|
|
52
|
+
# Empty when the session carries none.
|
|
53
|
+
harness_session_api_error() {
|
|
54
|
+
node -e "$harness_session_js"'
|
|
55
|
+
const d = session(process.argv[1]);
|
|
56
|
+
process.stdout.write(typeof d.api_error === "string" ? d.api_error : "");' "$1" 2>/dev/null
|
|
57
|
+
}
|
|
58
|
+
|
|
38
59
|
# harness_api_refused <session.json>: the session ended on an error that says
|
|
39
60
|
# the API cannot serve it: no credit, a quota, a spend or usage limit, billing,
|
|
40
61
|
# an invalid key, or the gateway itself failing (503 or another 5xx, a
|
|
@@ -43,12 +64,7 @@ HARNESS_CLAUDE=(env -u HARNESS_TELEGRAM_BOT_TOKEN -u HARNESS_TELEGRAM_CHAT_ID -u
|
|
|
43
64
|
# a task about authentication or billing never matches by its own words.
|
|
44
65
|
harness_api_refused() {
|
|
45
66
|
local text
|
|
46
|
-
text="$(
|
|
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
|
|
67
|
+
text="$(harness_session_error_text "$1")" || return 1
|
|
52
68
|
[[ -n "$text" ]] || return 1
|
|
53
69
|
harness_api_unreachable "$text" && return 0
|
|
54
70
|
harness_quota_transient "$text" && return 1
|
|
@@ -56,86 +72,30 @@ harness_api_refused() {
|
|
|
56
72
|
grep -qiE "credit balance|spend(ing)? limit|billing|usage limit|rate limit|out of usage|invalid api key|authentication" <<<"$text"
|
|
57
73
|
}
|
|
58
74
|
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
75
|
+
# ----------------------------------- machine layer shim (ADR 0026, section 9) --
|
|
76
|
+
# The session wrapper and the machine mode live in the machine layer,
|
|
77
|
+
# machine/session.sh, in the VPS runtime (HARNESS_WATCH_RUNTIME, default
|
|
78
|
+
# ~/.local/share/agent-harness/watch). HARNESS_MACHINE_API=1 is the contract.
|
|
79
|
+
# Without a runtime (CI, a laptop, a clone elsewhere) a session is a plain claude
|
|
80
|
+
# with the caller's own environment and the mode is api: the degraded fallback of
|
|
81
|
+
# ADR 0025, section 8. A key from HARNESS_ANTHROPIC_API_KEY is not used then.
|
|
82
|
+
# shellcheck disable=SC2034 # the contract marker, read by the runtime
|
|
83
|
+
HARNESS_MACHINE_API=1
|
|
84
|
+
harness_machine_session_file() { printf '%s' "${HARNESS_WATCH_RUNTIME:-$HOME/.local/share/agent-harness/watch}/machine/session.sh"; }
|
|
85
|
+
if [[ -r "$(harness_machine_session_file)" ]]; then
|
|
86
|
+
# shellcheck source=/dev/null
|
|
87
|
+
. "$(harness_machine_session_file)"
|
|
88
|
+
else
|
|
89
|
+
harness_mode() { echo api; }
|
|
90
|
+
harness_claude_session() { # <timeout-seconds> <session.json> <claude args...>
|
|
91
|
+
local secs="$1" out="$2" rc=0
|
|
92
|
+
shift 2
|
|
93
|
+
rm -f "$out.source"
|
|
94
|
+
timeout "$secs" "${HARNESS_CLAUDE[@]}" "$@" >"$out" 2>&1 || rc=$?
|
|
134
95
|
echo api >"$out.source"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
96
|
+
return "$rc"
|
|
97
|
+
}
|
|
98
|
+
fi
|
|
139
99
|
|
|
140
100
|
# Worktree where the task loop runs a change (ADR 0012), outside the checkout so
|
|
141
101
|
# lint, tests and type-checks of the main checkout never see it:
|
|
@@ -205,8 +165,30 @@ harness_claude_flags() {
|
|
|
205
165
|
# session limit") and sets api_error to usage_limit_reached, so both are matched;
|
|
206
166
|
# the gateway's Portuguese quota message is matched too, and its transient 429
|
|
207
167
|
# is not (quota.sh, ADR 0025 section 8).
|
|
168
|
+
#
|
|
169
|
+
# Only the session's own FAILURE is read: the api_error field, and the error
|
|
170
|
+
# text of a session that ended with is_error (harness_session_error_text).
|
|
171
|
+
# The answer of a session that ended well is never a usage error by what it
|
|
172
|
+
# says: a task that explains an old quota event ("cota semanal") is a task
|
|
173
|
+
# about a quota, not a quota. The one exception is the gateway's quota notice,
|
|
174
|
+
# which arrives unmarked, in the body of a 200: there the whole answer IS the
|
|
175
|
+
# notice, so a short answer that carries a quota marker still is a usage error
|
|
176
|
+
# (harness_quota_notice_unmarked).
|
|
208
177
|
harness_usage_error() {
|
|
209
|
-
|
|
178
|
+
local text reply
|
|
179
|
+
text="$(harness_session_error_text "$1")"
|
|
180
|
+
if [[ -n "$text" ]]; then
|
|
181
|
+
# The session failed: its error text (its result and its api_error) is the
|
|
182
|
+
# message the rule is about.
|
|
183
|
+
if harness_quota_exhausted "$text"; then return 0; fi
|
|
184
|
+
return 1
|
|
185
|
+
fi
|
|
186
|
+
# The session ended well: its answer is not a usage error by what it says.
|
|
187
|
+
# The api_error field still counts (usage_limit_reached), and the gateway's
|
|
188
|
+
# unmarked notice is the one answer that IS the notice.
|
|
189
|
+
if harness_quota_exhausted "$(harness_session_api_error "$1")"; then return 0; fi
|
|
190
|
+
reply="$(harness_session_reply "$1")" || return 1
|
|
191
|
+
harness_quota_notice_unmarked "$reply"
|
|
210
192
|
}
|
|
211
193
|
|
|
212
194
|
# harness_level_descend <level>: the level one below, for the fallback after a
|
|
@@ -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
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
# a quota marker and no transient marker. A 429 alone is not enough in
|
|
12
12
|
# either direction: both gateway messages carry the same structured
|
|
13
13
|
# fields, so only the text tells them apart.
|
|
14
|
+
# (c) is a session that ENDED WELL the gateway's quota notice?
|
|
15
|
+
# harness_quota_notice_unmarked <text>
|
|
16
|
+
# the gateway sometimes answers 200 with the quota notice in the body,
|
|
17
|
+
# without marking it as an error: there the whole answer is the notice.
|
|
14
18
|
# A transient 429 (harness_quota_transient) is neither: it may be retried and
|
|
15
19
|
# changes nothing.
|
|
16
20
|
#
|
|
@@ -34,8 +38,11 @@ harness_quota_markers='limite de uso do seu plano|cota semanal|sera resetado em|
|
|
|
34
38
|
# Transient markers, on normalized text: the request may be retried.
|
|
35
39
|
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
40
|
|
|
37
|
-
# Gateway failures, on normalized text: the API does not answer.
|
|
38
|
-
|
|
41
|
+
# Gateway failures, on normalized text: the API does not answer. This is the
|
|
42
|
+
# one list for the whole system (ADR 0026, section 6): the machine library
|
|
43
|
+
# (vps-bootstrap) loads this file and keeps a copy only as a degraded fallback,
|
|
44
|
+
# and a test there fails if the two disagree.
|
|
45
|
+
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
46
|
|
|
40
47
|
# harness_quota_transient <text>: a retryable condition, not a quota.
|
|
41
48
|
harness_quota_transient() {
|
|
@@ -49,8 +56,38 @@ harness_quota_exhausted() {
|
|
|
49
56
|
grep -qE -- "$harness_quota_markers" <<<"$t" && ! grep -qE -- "$harness_quota_transient_markers" <<<"$t"
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
# The longest text that can be the gateway's quota notice. The notice arrives
|
|
60
|
+
# in the body of a 200 response, without being marked as an error (the rule of
|
|
61
|
+
# the watchdog, vps-bootstrap, 2026-11-11): there the whole answer IS the
|
|
62
|
+
# notice. A long answer that talks about a quota is a task about a quota, not a
|
|
63
|
+
# quota.
|
|
64
|
+
harness_quota_notice_max_len=400
|
|
65
|
+
|
|
66
|
+
# harness_quota_notice_unmarked <text>: the text is the gateway's quota notice
|
|
67
|
+
# that arrived unmarked: a SHORT message (at most harness_quota_notice_max_len
|
|
68
|
+
# characters) that carries a quota marker. The markers are the ones above (one
|
|
69
|
+
# list, ADR 0025 section 8): this rule only says how much of the answer may
|
|
70
|
+
# be the notice.
|
|
71
|
+
harness_quota_notice_unmarked() {
|
|
72
|
+
local t="$1" len
|
|
73
|
+
len=${#t}
|
|
74
|
+
(( len <= harness_quota_notice_max_len )) || return 1
|
|
75
|
+
harness_quota_exhausted "$t"
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
# harness_api_unreachable <text>: the gateway failed (503 or another 5xx, a
|
|
53
79
|
# connection failure, a timeout): the API does not answer, whatever the quota.
|
|
54
80
|
harness_api_unreachable() {
|
|
55
81
|
grep -qE -- "$harness_gateway_markers" <<<"$(harness_quota_normalize "$1")"
|
|
56
82
|
}
|
|
83
|
+
|
|
84
|
+
# harness_api_fault_class <text>: how a session error counts for the machine
|
|
85
|
+
# mode (ADR 0025 section 8, ADR 0026 sections 3 and 6). Prints one word:
|
|
86
|
+
# transient a passing failure: counts only after several in a row
|
|
87
|
+
# down the API does not serve: a quota, or a gateway failure
|
|
88
|
+
# none not a failure of the API
|
|
89
|
+
harness_api_fault_class() {
|
|
90
|
+
if harness_quota_transient "$1"; then printf 'transient'; return 0; fi
|
|
91
|
+
if harness_quota_exhausted "$1" || harness_api_unreachable "$1"; then printf 'down'; return 0; fi
|
|
92
|
+
printf 'none'
|
|
93
|
+
}
|