@data-matters.io/agent-harness 0.1.4 → 0.1.5
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,57 @@ 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 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_claude_session() {
|
|
58
|
+
local secs="$1" out="$2" key="${HARNESS_ANTHROPIC_API_KEY:-}" rc=0 prompt
|
|
59
|
+
shift 2
|
|
60
|
+
rm -f "$out.source"
|
|
61
|
+
if [[ -z "$key" ]]; then
|
|
62
|
+
timeout "$secs" "${HARNESS_CLAUDE[@]}" "$@" >"$out" 2>&1 || rc=$?
|
|
63
|
+
return "$rc"
|
|
64
|
+
fi
|
|
65
|
+
prompt="$(mktemp)"
|
|
66
|
+
cat >"$prompt"
|
|
67
|
+
ANTHROPIC_API_KEY="$key" timeout "$secs" "${HARNESS_CLAUDE[@]}" "$@" <"$prompt" >"$out" 2>&1 || rc=$?
|
|
68
|
+
# Whatever the session printed, the key itself never stays in the log.
|
|
69
|
+
K="$key" node -e 'const fs = require("fs"); const f = process.argv[1];
|
|
70
|
+
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
|
|
71
|
+
if (( rc != 124 )) && harness_api_refused "$out"; then
|
|
72
|
+
echo "harness: the API key was refused; repeating the session on the plan login" >&2
|
|
73
|
+
rc=0
|
|
74
|
+
timeout "$secs" env -u ANTHROPIC_API_KEY "${HARNESS_CLAUDE[@]}" "$@" <"$prompt" >"$out" 2>&1 || rc=$?
|
|
75
|
+
echo plan >"$out.source"
|
|
76
|
+
else
|
|
77
|
+
echo api >"$out.source"
|
|
78
|
+
fi
|
|
79
|
+
rm -f "$prompt"
|
|
80
|
+
return "$rc"
|
|
81
|
+
}
|
|
32
82
|
|
|
33
83
|
# Worktree where the task loop runs a change (ADR 0012), outside the checkout so
|
|
34
84
|
# lint, tests and type-checks of the main checkout never see it:
|
|
@@ -158,14 +208,32 @@ harness_session_usage() {
|
|
|
158
208
|
|
|
159
209
|
# harness_record_usage <project> <ref> <task-type> <level> <effort> <attempt> <result> <session.json>:
|
|
160
210
|
# one line in the machine-wide ledger (HARNESS_USAGE_LEDGER, default
|
|
161
|
-
# ~/.local/state/harness/usage.tsv); prints the session's usage columns.
|
|
211
|
+
# ~/.local/state/harness/usage.tsv); prints the session's usage columns. The last
|
|
212
|
+
# column, source, is api or plan (harness_claude_session); a ledger written
|
|
213
|
+
# before it existed gets the column added to its header, its old rows stay empty.
|
|
162
214
|
harness_record_usage() {
|
|
163
|
-
local ledger="${HARNESS_USAGE_LEDGER:-$HOME/.local/state/harness/usage.tsv}" usage
|
|
215
|
+
local ledger="${HARNESS_USAGE_LEDGER:-$HOME/.local/state/harness/usage.tsv}" usage source
|
|
164
216
|
usage="$(harness_session_usage "$8")"
|
|
217
|
+
source="$(harness_session_source "$8")"
|
|
165
218
|
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"
|
|
219
|
+
[[ -s "$ledger" ]] || printf 'timestamp\tproject\tref\ttask_type\tlevel\tmodel\teffort\tcost_usd\toutput_tokens\tcache_read_tokens\tresult\tattempt\tsource\n' >"$ledger"
|
|
220
|
+
harness_tsv_add_source "$ledger"
|
|
167
221
|
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"
|
|
222
|
+
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" \
|
|
223
|
+
"$model" "${5:-default}" "$cost" "$out" "$cache" "$7" "$6" "$source" >>"$ledger"
|
|
170
224
|
printf '%s' "$usage"
|
|
171
225
|
}
|
|
226
|
+
|
|
227
|
+
# harness_session_source <session.json>: api or plan, as harness_claude_session
|
|
228
|
+
# recorded it (plan when it did not: no API key was configured).
|
|
229
|
+
harness_session_source() {
|
|
230
|
+
local s=plan
|
|
231
|
+
[[ -f "$1.source" ]] && s="$(<"$1.source")"
|
|
232
|
+
echo "$s"
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# harness_tsv_add_source <file>: adds the source column to the header of a TSV
|
|
236
|
+
# written before it existed.
|
|
237
|
+
harness_tsv_add_source() {
|
|
238
|
+
[[ "$(head -n1 "$1")" == *$'\tsource' ]] || sed -i '1s/$/\tsource/' "$1"
|
|
239
|
+
}
|
|
@@ -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
|