@data-matters.io/agent-harness 0.1.4 → 0.1.6
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
|
@@ -28,7 +28,69 @@ harness_load_env() {
|
|
|
28
28
|
# the session's environment: only notify.sh needs them, and it reads the env file
|
|
29
29
|
# itself. TELEGRAM_STATE_DIR from the env file is kept.
|
|
30
30
|
# shellcheck disable=SC2034 # used by the scripts that source this file
|
|
31
|
-
HARNESS_CLAUDE=(env -u HARNESS_TELEGRAM_BOT_TOKEN -u HARNESS_TELEGRAM_CHAT_ID claude)
|
|
31
|
+
HARNESS_CLAUDE=(env -u HARNESS_TELEGRAM_BOT_TOKEN -u HARNESS_TELEGRAM_CHAT_ID -u HARNESS_ANTHROPIC_API_KEY -u HARNESS_ANTHROPIC_BASE_URL claude)
|
|
32
|
+
|
|
33
|
+
# ------------------------------------------- optional API key (ADR 0020) --
|
|
34
|
+
# harness_api_refused <session.json>: the session ended on an error that says
|
|
35
|
+
# the Anthropic API key cannot serve it: no credit, a spend or usage limit, a
|
|
36
|
+
# rate limit, billing, or an invalid key. Only an errored session counts, so a
|
|
37
|
+
# task about authentication or billing never matches by its own words.
|
|
38
|
+
harness_api_refused() {
|
|
39
|
+
node -e "$harness_session_js"'
|
|
40
|
+
const raw = (() => { try { return require("fs").readFileSync(process.argv[1], "utf8"); } catch { return ""; } })();
|
|
41
|
+
const d = session(process.argv[1]);
|
|
42
|
+
const text = Object.keys(d).length
|
|
43
|
+
? (d.is_error ? [d.result, d.api_error].filter((x) => typeof x === "string").join(" ") : "")
|
|
44
|
+
: raw;
|
|
45
|
+
process.exitCode = /credit balance|spend(ing)? limit|billing|usage limit|rate limit|out of usage|invalid api key|authentication/i.test(text) ? 0 : 1;' "$1" 2>/dev/null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# harness_claude_session <timeout-seconds> <session.json> <claude args...>: runs
|
|
49
|
+
# one headless Claude Code session (the prompt on stdin) and writes its output
|
|
50
|
+
# (stdout and stderr) to <session.json>; returns the exit status of the run
|
|
51
|
+
# that produced it (124: timed out). Without HARNESS_ANTHROPIC_API_KEY this is
|
|
52
|
+
# the account login, exactly as before. With it, the session runs on the API
|
|
53
|
+
# first (the key goes to that process only, through its environment, never its
|
|
54
|
+
# arguments or a log); when the API refuses it (harness_api_refused), the SAME
|
|
55
|
+
# attempt repeats on the plan login. Which one produced the output is written to
|
|
56
|
+
# <session.json>.source (api|plan), which the usage ledger reads.
|
|
57
|
+
# HARNESS_ANTHROPIC_BASE_URL is the optional base URL of that key (a gateway):
|
|
58
|
+
# it reaches the API run only, as ANTHROPIC_BASE_URL, and the plan run never
|
|
59
|
+
# sees it. A key from a gateway without its base URL would be sent to the
|
|
60
|
+
# Anthropic API and refused.
|
|
61
|
+
harness_claude_session() {
|
|
62
|
+
local secs="$1" out="$2" key="${HARNESS_ANTHROPIC_API_KEY:-}" base="${HARNESS_ANTHROPIC_BASE_URL:-}" rc=0 prompt
|
|
63
|
+
shift 2
|
|
64
|
+
rm -f "$out.source"
|
|
65
|
+
if [[ -z "$key" ]]; then
|
|
66
|
+
timeout "$secs" "${HARNESS_CLAUDE[@]}" "$@" >"$out" 2>&1 || rc=$?
|
|
67
|
+
return "$rc"
|
|
68
|
+
fi
|
|
69
|
+
prompt="$(mktemp)"
|
|
70
|
+
cat >"$prompt"
|
|
71
|
+
if [[ -n "$base" ]]; then
|
|
72
|
+
ANTHROPIC_API_KEY="$key" ANTHROPIC_BASE_URL="$base" timeout "$secs" "${HARNESS_CLAUDE[@]}" "$@" <"$prompt" >"$out" 2>&1 || rc=$?
|
|
73
|
+
else
|
|
74
|
+
ANTHROPIC_API_KEY="$key" timeout "$secs" "${HARNESS_CLAUDE[@]}" "$@" <"$prompt" >"$out" 2>&1 || rc=$?
|
|
75
|
+
fi
|
|
76
|
+
# Whatever the session printed, the key itself never stays in the log.
|
|
77
|
+
K="$key" node -e 'const fs = require("fs"); const f = process.argv[1];
|
|
78
|
+
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
|
|
79
|
+
if (( rc != 124 )) && harness_api_refused "$out"; then
|
|
80
|
+
echo "harness: the API key was refused; repeating the session on the plan login" >&2
|
|
81
|
+
rc=0
|
|
82
|
+
if [[ -n "$base" ]]; then
|
|
83
|
+
timeout "$secs" env -u ANTHROPIC_API_KEY -u ANTHROPIC_BASE_URL "${HARNESS_CLAUDE[@]}" "$@" <"$prompt" >"$out" 2>&1 || rc=$?
|
|
84
|
+
else
|
|
85
|
+
timeout "$secs" env -u ANTHROPIC_API_KEY "${HARNESS_CLAUDE[@]}" "$@" <"$prompt" >"$out" 2>&1 || rc=$?
|
|
86
|
+
fi
|
|
87
|
+
echo plan >"$out.source"
|
|
88
|
+
else
|
|
89
|
+
echo api >"$out.source"
|
|
90
|
+
fi
|
|
91
|
+
rm -f "$prompt"
|
|
92
|
+
return "$rc"
|
|
93
|
+
}
|
|
32
94
|
|
|
33
95
|
# Worktree where the task loop runs a change (ADR 0012), outside the checkout so
|
|
34
96
|
# lint, tests and type-checks of the main checkout never see it:
|
|
@@ -158,14 +220,32 @@ harness_session_usage() {
|
|
|
158
220
|
|
|
159
221
|
# harness_record_usage <project> <ref> <task-type> <level> <effort> <attempt> <result> <session.json>:
|
|
160
222
|
# one line in the machine-wide ledger (HARNESS_USAGE_LEDGER, default
|
|
161
|
-
# ~/.local/state/harness/usage.tsv); prints the session's usage columns.
|
|
223
|
+
# ~/.local/state/harness/usage.tsv); prints the session's usage columns. The last
|
|
224
|
+
# column, source, is api or plan (harness_claude_session); a ledger written
|
|
225
|
+
# before it existed gets the column added to its header, its old rows stay empty.
|
|
162
226
|
harness_record_usage() {
|
|
163
|
-
local ledger="${HARNESS_USAGE_LEDGER:-$HOME/.local/state/harness/usage.tsv}" usage
|
|
227
|
+
local ledger="${HARNESS_USAGE_LEDGER:-$HOME/.local/state/harness/usage.tsv}" usage source
|
|
164
228
|
usage="$(harness_session_usage "$8")"
|
|
229
|
+
source="$(harness_session_source "$8")"
|
|
165
230
|
mkdir -p "$(dirname "$ledger")"
|
|
166
|
-
[[ -s "$ledger" ]] || printf 'timestamp\tproject\tref\ttask_type\tlevel\tmodel\teffort\tcost_usd\toutput_tokens\tcache_read_tokens\tresult\tattempt\n' >"$ledger"
|
|
231
|
+
[[ -s "$ledger" ]] || printf 'timestamp\tproject\tref\ttask_type\tlevel\tmodel\teffort\tcost_usd\toutput_tokens\tcache_read_tokens\tresult\tattempt\tsource\n' >"$ledger"
|
|
232
|
+
harness_tsv_add_source "$ledger"
|
|
167
233
|
IFS=$'\t' read -r model cost out cache <<<"$usage"
|
|
168
|
-
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$(date -Iseconds)" "$1" "$2" "$3" "$4" \
|
|
169
|
-
"$model" "${5:-default}" "$cost" "$out" "$cache" "$7" "$6" >>"$ledger"
|
|
234
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$(date -Iseconds)" "$1" "$2" "$3" "$4" \
|
|
235
|
+
"$model" "${5:-default}" "$cost" "$out" "$cache" "$7" "$6" "$source" >>"$ledger"
|
|
170
236
|
printf '%s' "$usage"
|
|
171
237
|
}
|
|
238
|
+
|
|
239
|
+
# harness_session_source <session.json>: api or plan, as harness_claude_session
|
|
240
|
+
# recorded it (plan when it did not: no API key was configured).
|
|
241
|
+
harness_session_source() {
|
|
242
|
+
local s=plan
|
|
243
|
+
[[ -f "$1.source" ]] && s="$(<"$1.source")"
|
|
244
|
+
echo "$s"
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# harness_tsv_add_source <file>: adds the source column to the header of a TSV
|
|
248
|
+
# written before it existed.
|
|
249
|
+
harness_tsv_add_source() {
|
|
250
|
+
[[ "$(head -n1 "$1")" == *$'\tsource' ]] || sed -i '1s/$/\tsource/' "$1"
|
|
251
|
+
}
|
|
@@ -207,11 +207,13 @@ trap '[[ "$run_status" == running ]] && run_status=interrupted; write_state' EXI
|
|
|
207
207
|
write_state
|
|
208
208
|
cd "$wt"
|
|
209
209
|
summary="$log_dir/summary.tsv"
|
|
210
|
-
summary_header=$'timestamp\ttask\tattempt\tduration_s\tresult\tlevel\tmodel\teffort\tcost_usd\toutput_tokens\tcache_read_tokens'
|
|
210
|
+
summary_header=$'timestamp\ttask\tattempt\tduration_s\tresult\tlevel\tmodel\teffort\tcost_usd\toutput_tokens\tcache_read_tokens\tsource'
|
|
211
211
|
if [[ ! -f "$summary" ]]; then
|
|
212
212
|
printf '%s\n' "$summary_header" >"$summary"
|
|
213
213
|
elif [[ "$(head -n1 "$summary")" == $'timestamp\ttask\tattempt\tduration_s\tresult' ]]; then
|
|
214
214
|
sed -i "1s/.*/$summary_header/" "$summary" # older runs: same first columns, new ones empty
|
|
215
|
+
else
|
|
216
|
+
harness_tsv_add_source "$summary"
|
|
215
217
|
fi
|
|
216
218
|
|
|
217
219
|
next_task() { grep -n -m1 -E '^[[:space:]]*- \[ \] ' "$tasks" || true; }
|
|
@@ -297,13 +299,14 @@ warn_if_untrusted() {
|
|
|
297
299
|
# with its level, model, effort, cost and tokens (ADR 0020); the same session
|
|
298
300
|
# also goes to the machine-wide ledger.
|
|
299
301
|
record() {
|
|
300
|
-
local usage effort model cost out cache
|
|
302
|
+
local usage effort model cost out cache source
|
|
301
303
|
effort="$(harness_flag_value "${level_flags:-}" --effort)"
|
|
302
304
|
usage="$(harness_record_usage "$(basename "$root")" "$change" impl "${level:-}" "${effort:-default}" "$2" "$4" \
|
|
303
305
|
"$log_dir/task-$1-attempt-$2.json")"
|
|
304
306
|
IFS=$'\t' read -r model cost out cache <<<"$usage"
|
|
305
|
-
|
|
306
|
-
|
|
307
|
+
source="$(harness_session_source "$log_dir/task-$1-attempt-$2.json")"
|
|
308
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$(date -Iseconds)" "$1" "$2" "$3" "$4" \
|
|
309
|
+
"${level:-}" "$model" "${effort:-default}" "$cost" "$out" "$cache" "$source" >>"$summary"
|
|
307
310
|
}
|
|
308
311
|
|
|
309
312
|
while line="$(next_task)"; [[ -n "$line" ]]; do
|
|
@@ -345,13 +348,12 @@ while line="$(next_task)"; [[ -n "$line" ]]; do
|
|
|
345
348
|
session_json="$log_dir/task-$task_id-attempt-$attempt.json"
|
|
346
349
|
while :; do
|
|
347
350
|
# shellcheck disable=SC2086
|
|
348
|
-
build_prompt "$task" "$feedback" |
|
|
351
|
+
build_prompt "$task" "$feedback" | harness_claude_session "$timeout_s" "$session_json" -p \
|
|
349
352
|
--output-format json \
|
|
350
353
|
--permission-mode "$permission_mode" \
|
|
351
354
|
--disallowedTools "Bash(git commit:*)" "Bash(git push:*)" "Bash(.harness/spec-pr.sh:*)" \
|
|
352
355
|
"Bash(pnpm add:*)" "Bash(pnpm remove:*)" "Bash(pnpm update:*)" "Bash(npm install:*)" \
|
|
353
|
-
${level_flags} ${HARNESS_CLAUDE_ARGS:-}
|
|
354
|
-
>"$session_json" 2>&1 || true
|
|
356
|
+
${level_flags} ${HARNESS_CLAUDE_ARGS:-} || true
|
|
355
357
|
|
|
356
358
|
warn_if_untrusted "$session_json"
|
|
357
359
|
|
|
@@ -462,10 +464,10 @@ if [[ -x .harness/gates.sh ]] && .harness/gates.sh --has-final; then
|
|
|
462
464
|
session_json="$log_dir/task-final-attempt-$attempt.json"
|
|
463
465
|
# shellcheck disable=SC2086
|
|
464
466
|
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
|
-
|
|
|
467
|
+
| harness_claude_session "$timeout_s" "$session_json" -p --output-format json --permission-mode "$permission_mode" \
|
|
466
468
|
--disallowedTools "Bash(git commit:*)" "Bash(git push:*)" "Bash(.harness/spec-pr.sh:*)" \
|
|
467
469
|
"Bash(pnpm add:*)" "Bash(pnpm remove:*)" "Bash(pnpm update:*)" "Bash(npm install:*)" \
|
|
468
|
-
${level_flags} ${HARNESS_CLAUDE_ARGS:-}
|
|
470
|
+
${level_flags} ${HARNESS_CLAUDE_ARGS:-} || true
|
|
469
471
|
warn_if_untrusted "$session_json"
|
|
470
472
|
if harness_usage_error "$session_json"; then
|
|
471
473
|
record final "$attempt" "$(( $(date +%s) - start ))" paused
|