chore: improve harness guidance and ACP debugging (#1969)

This commit is contained in:
Carlos Florêncio
2026-08-01 18:35:14 +01:00
committed by GitHub
parent 1bccadf8f9
commit 5182cfb95a
26 changed files with 1117 additions and 88 deletions
+2 -2
View File
@@ -286,11 +286,11 @@ WEB_STARTED=0
if [[ "$WANT_WEB" -eq 1 ]]; then
echo "dev-isolated: starting web (vite dev) on :$WEB_PORT ..."
env \
NEXT_PUBLIC_KANDEV_API_PORT="$BACKEND_PORT" \
VITE_KANDEV_API_PORT="$BACKEND_PORT" \
KANDEV_API_BASE_URL="$BACKEND_URL" \
PORT="$WEB_PORT" \
HOSTNAME="127.0.0.1" \
NEXT_PUBLIC_KANDEV_DEBUG="true" \
VITE_KANDEV_DEBUG="true" \
setsid pnpm -C "$REPO_ROOT/apps" --filter @kandev/web dev >"$WEB_LOG" 2>&1 &
WEB_PID=$!
echo "$WEB_PID" >"$WEB_PIDFILE"
+125 -5
View File
@@ -4,14 +4,17 @@
# Usage:
# scripts/pr-state [--summary] [--all] [--trusted-reviewer <login>] <PR>
# scripts/pr-state --comment <comment_id>
# scripts/pr-state --job-log <job_id>
#
# Raw output:
# JSON object with:
# - pr: { number, branch }
# - pr: { number, branch, is_cross_repository, head_repository_owner,
# head_repository_name, head_ref_name, head_ref_oid, maintainer_can_modify }
# - since: { head_oid, committed_at } or null
# - checks: [{ type, name, workflow, status, conclusion, details_url, target_url, run_id, job_id }]
# - checks_head_sha: <SHA or null>
# - checks_snapshot_complete: <boolean>
# - approval_required_runs: [{ run_id, name, workflow, status, conclusion, head_sha, url }]
# - review_threads: [{ thread_id, is_resolved, path, comment_id, comment_created_at, author, body_preview }]
# - hidden_unresolved_threads: [{ thread_id, comment_id, author, path, body_preview }]
# - unresolved_review_thread_count: <N>
@@ -27,7 +30,8 @@
#
# Summary output (--summary):
# JSON object with:
# - pr: { number, branch }
# - pr: { number, branch, is_cross_repository, head_repository_owner,
# head_repository_name, head_ref_name, head_ref_oid, maintainer_can_modify }
# - since: { head_oid, committed_at } or null
# - failed_checks: [{ name, workflow, status, conclusion, run_id, job_id, details_url, target_url }]
# - pending_checks: [{ name, workflow, status, run_id, job_id, details_url, target_url }]
@@ -35,6 +39,7 @@
# - passed_check_count: <N>
# - checks_head_sha: <SHA or null>
# - checks_snapshot_complete: <boolean>
# - approval_required_runs: [{ run_id, name, workflow, status, conclusion, head_sha, url }]
# - unresolved_review_thread_count: <N>
# - filtered_review_thread_count: <N>
# - unresolved_threads: [{ thread_id, comment_id, author, path, body_preview }]
@@ -52,6 +57,10 @@
# JSON object for one PR review comment:
# - comment_id, author, body, path, line, original_line, commit_id, html_url, created_at, updated_at
#
# Job-log output (--job-log):
# Bounded context around the most likely failing test or step. The GitHub
# endpoint may return either plain text or a ZIP archive.
#
# By default, comments/reviews/review threads are limited to items created after
# the PR head commit. Checks are already scoped by GitHub to the current head.
# Pass --all to include historical comments and reviews.
@@ -69,6 +78,7 @@ INCLUDE_ALL=0
SUMMARY=0
TRUSTED_REVIEWER=''
COMMENT_ID=''
JOB_LOG_ID=''
PR=''
while [[ $# -gt 0 ]]; do
case "$1" in
@@ -94,6 +104,13 @@ while [[ $# -gt 0 ]]; do
COMMENT_ID=$1
shift
;;
--job-log)
shift
[[ $# -gt 0 && -z "${JOB_LOG_ID:-}" && -z "${PR:-}" ]] || usage 1
[[ "$1" =~ ^[0-9]+$ ]] || usage 1
JOB_LOG_ID=$1
shift
;;
-h | --help)
usage 0
;;
@@ -119,7 +136,11 @@ done
[ $# -eq 0 ] || usage
if [[ -n "${COMMENT_ID:-}" ]]; then
if [[ -n "${COMMENT_ID:-}" && -n "${JOB_LOG_ID:-}" ]]; then
usage 1
elif [[ -n "${COMMENT_ID:-}" ]]; then
[[ -z "${PR:-}" && "$SUMMARY" == "0" && "$INCLUDE_ALL" == "0" ]] || usage
elif [[ -n "${JOB_LOG_ID:-}" ]]; then
[[ -z "${PR:-}" && "$SUMMARY" == "0" && "$INCLUDE_ALL" == "0" ]] || usage
else
[ -n "${PR:-}" ] || usage
@@ -130,6 +151,7 @@ pr_json='{}'
checks_json='[]'
checks_head_sha=''
checks_snapshot_complete=false
approval_required_runs_json='[]'
review_threads_json='[]'
hidden_unresolved_threads_json='[]'
reviews_json='[]'
@@ -187,11 +209,58 @@ emit_comment_json() {
}' <<<"$comment_json"
}
emit_job_log() {
local job_id=$1
local repo_json owner repo archive_file log_file magic
archive_file="$(mktemp)"
log_file="$(mktemp)"
repo_json="$(gh_json repo view --json owner,name)" || {
rm -f "$archive_file" "$log_file"
printf 'failed to resolve repository identity\n' >&2
return 1
}
owner="$(jq -r '.owner.login // ""' <<<"$repo_json")"
repo="$(jq -r '.name // ""' <<<"$repo_json")"
if [[ -z "$owner" || -z "$repo" ]]; then
rm -f "$archive_file" "$log_file"
printf 'failed to resolve repository identity\n' >&2
return 1
fi
if ! gh_json api "repos/$owner/$repo/actions/jobs/$job_id/logs" >"$archive_file"; then
rm -f "$archive_file" "$log_file"
printf 'failed to fetch job log %s\n' "$job_id" >&2
return 1
fi
magic="$(head -c 4 "$archive_file" || true)"
if [[ "$magic" == $'PK\003\004' ]]; then
if ! unzip -p "$archive_file" >"$log_file"; then
rm -f "$archive_file" "$log_file"
printf 'failed to unpack job log %s\n' "$job_id" >&2
return 1
fi
else
mv "$archive_file" "$log_file"
archive_file=''
fi
if ! rg -n -i -C 4 '(Test timeout|Error:|FAIL|failed|Process completed)' "$log_file" | sed -n '1,240p'; then
tail -n 80 "$log_file"
fi
rm -f "$archive_file" "$log_file"
}
if [[ -n "${COMMENT_ID:-}" ]]; then
emit_comment_json "$COMMENT_ID"
exit $?
fi
if [[ -n "${JOB_LOG_ID:-}" ]]; then
emit_job_log "$JOB_LOG_ID"
exit $?
fi
extract_checks_json() {
local status_json=$1
printf '%s\n' "$status_json" | jq -c '
@@ -491,6 +560,7 @@ summarize_state_json() {
since,
checks_head_sha,
checks_snapshot_complete,
approval_required_runs: (.approval_required_runs // []),
failed_checks: [
effective_checks[]
| select(terminal_failure_status and bad_conclusion)
@@ -538,6 +608,39 @@ summarize_state_json() {
'
}
fetch_approval_required_runs_json() {
local owner=$1 repo=$2 pr_number=$3 head_sha=$4
if [[ "$is_cross_repository" != "true" || -z "$owner" || -z "$repo" || -z "$pr_number" || -z "$head_sha" ]]; then
printf '%s\n' '[]'
return
fi
local runs_pages
if ! runs_pages="$(gh_json api --paginate "repos/$owner/$repo/actions/runs?event=pull_request&head_sha=$head_sha&per_page=100")"; then
return 1
fi
jq -cs --argjson pr "$pr_number" --arg head "$head_sha" '
[.[].workflow_runs[]?
| select(
.status == "completed"
and .conclusion == "action_required"
and .head_sha == $head
and any(.pull_requests[]?; .number == $pr and .head.sha == $head)
)
| {
run_id: (.id | tostring),
name: (.name // "unknown"),
workflow: (.path // null),
status: (.status // null),
conclusion: (.conclusion // null),
head_sha: (.head_sha // null),
url: (.html_url // null)
}
] | sort_by(.run_id | tonumber)
' <<<"$runs_pages"
}
fetch_head_commit_json() {
local owner=$1
local repo=$2
@@ -658,13 +761,22 @@ comments_json='[]'
status_json='[]'
resolved_pr_number=''
is_cross_repository=null
if pr_view_json="$(gh_json pr view "$PR" --json number,headRefName,headRefOid,isCrossRepository,statusCheckRollup,comments,url)"; then
if pr_view_json="$(gh_json pr view "$PR" --json number,headRefName,headRefOid,headRepositoryOwner,headRepository,maintainerCanModify,isCrossRepository,statusCheckRollup,comments,url)"; then
resolved_pr_number="$(jq -r '.number // ""' <<<"$pr_view_json")"
if pr_url_owner="$(jq -r '.url | capture("github.com/(?<owner>[^/]+)/(?<repo>[^/]+)/pull/[0-9]+").owner // ""' <<<"$pr_view_json" 2>/dev/null)" && [[ -n "$pr_url_owner" ]]; then
repo_owner="$pr_url_owner"
repo_name="$(jq -r '.url | capture("github.com/(?<owner>[^/]+)/(?<repo>[^/]+)/pull/[0-9]+").repo // ""' <<<"$pr_view_json")"
fi
pr_json="$(jq -c '{number: .number, branch: (.headRefName // "unknown"), is_cross_repository: (if has("isCrossRepository") then .isCrossRepository else null end)}' <<<"$pr_view_json")"
pr_json="$(jq -c '{
number: .number,
branch: (.headRefName // "unknown"),
is_cross_repository: (if has("isCrossRepository") then .isCrossRepository else null end),
head_repository_owner: (.headRepositoryOwner.login // null),
head_repository_name: (.headRepository.name // null),
head_ref_name: (.headRefName // null),
head_ref_oid: (.headRefOid // null),
maintainer_can_modify: (.maintainerCanModify // null)
}' <<<"$pr_view_json")"
is_cross_repository="$(jq -c 'if has("isCrossRepository") then .isCrossRepository else null end' <<<"$pr_view_json")"
comments_json="$(jq -c '.comments // []' <<<"$pr_view_json")"
status_json="$(jq -c '.statusCheckRollup // []' <<<"$pr_view_json")"
@@ -679,6 +791,11 @@ else
add_error "pr_view" "gh pr view failed"
fi
if ! approval_required_runs_json="$(fetch_approval_required_runs_json "$repo_owner" "$repo_name" "$resolved_pr_number" "$checks_head_sha")"; then
approval_required_runs_json='[]'
add_error "approval_required_runs" "workflow runs API failed"
fi
since=''
if [[ "$INCLUDE_ALL" != "1" && -n "$repo_owner" && -n "$repo_name" && -n "$resolved_pr_number" ]]; then
if since_json="$(fetch_head_commit_json "$repo_owner" "$repo_name" "$resolved_pr_number")"; then
@@ -765,6 +882,7 @@ trap cleanup_state_tmp EXIT
printf '%s\n' "$pr_json" >"$state_tmp_dir/pr.json"
printf '%s\n' "$since_json" >"$state_tmp_dir/since.json"
printf '%s\n' "$checks_json" >"$state_tmp_dir/checks.json"
printf '%s\n' "$approval_required_runs_json" >"$state_tmp_dir/approval_required_runs.json"
printf '%s\n' "$review_threads_json" >"$state_tmp_dir/review_threads.json"
printf '%s\n' "$hidden_unresolved_threads_json" >"$state_tmp_dir/hidden_unresolved_threads.json"
printf '%s\n' "$reviews_json" >"$state_tmp_dir/reviews.json"
@@ -776,6 +894,7 @@ state_json="$(jq -cn \
--slurpfile pr "$state_tmp_dir/pr.json" \
--slurpfile since "$state_tmp_dir/since.json" \
--slurpfile checks "$state_tmp_dir/checks.json" \
--slurpfile approval_required_runs "$state_tmp_dir/approval_required_runs.json" \
--slurpfile review_threads "$state_tmp_dir/review_threads.json" \
--slurpfile hidden_unresolved_threads "$state_tmp_dir/hidden_unresolved_threads.json" \
--slurpfile reviews "$state_tmp_dir/reviews.json" \
@@ -792,6 +911,7 @@ state_json="$(jq -cn \
checks: $checks[0],
checks_head_sha: (if $checks_head_sha == "" then null else $checks_head_sha end),
checks_snapshot_complete: $checks_snapshot_complete,
approval_required_runs: $approval_required_runs[0],
review_threads: $review_threads[0],
hidden_unresolved_threads: $hidden_unresolved_threads[0],
unresolved_review_thread_count: $unresolved_review_thread_count,
+117
View File
@@ -69,6 +69,11 @@ if [[ "${GH_FAIL_COMMENT:-0}" == "1" && "$1" == "api" && "$2" == repos/kdlbs/kan
exit 1
fi
if [[ "${GH_FAIL_APPROVAL_RUNS:-0}" == "1" && "$*" == *"repos/kdlbs/kandev/actions/runs?event=pull_request&head_sha=abc123&per_page=100"* ]]; then
echo "workflow runs api failed" >&2
exit 1
fi
if [[ "$1" == "repo" && "$2" == "view" ]]; then
printf '{"owner":{"login":"kdlbs"},"name":"kandev"}\n'
exit 0
@@ -92,6 +97,41 @@ JSON
exit 0
fi
if [[ "$*" == *"repos/kdlbs/kandev/actions/runs?event=pull_request&head_sha=abc123&per_page=100"* ]]; then
if [[ "${GH_APPROVAL_REQUIRED:-0}" == "1" ]]; then
cat <<'JSON'
{"workflow_runs":[{"id":444,"name":"Backend tests","path":".github/workflows/ci.yml","event":"pull_request","status":"completed","conclusion":"action_required","head_sha":"abc123","html_url":"https://github.com/kdlbs/kandev/actions/runs/444","pull_requests":[{"number":123,"head":{"sha":"abc123"}}]},{"id":443,"name":"Stale run","path":".github/workflows/ci.yml","event":"pull_request","status":"completed","conclusion":"action_required","head_sha":"old-head","html_url":"https://github.com/kdlbs/kandev/actions/runs/443","pull_requests":[{"number":123,"head":{"sha":"old-head"}}]}]}
JSON
else
printf '%s\n' '{"workflow_runs":[]}'
fi
exit 0
fi
if [[ "$1" == "api" && "$2" == "repos/kdlbs/kandev/actions/jobs/55150000002/logs" ]]; then
log_file="$(mktemp)"
{
printf '%s\n' 'setup complete'
printf '%s\n' 'Test timeout: expected ready state'
printf '%s\n' 'stack frame one'
printf '%s\n' 'stack frame two'
printf '%s\n' 'stack frame three'
printf '%s\n' 'stack frame four'
printf '%s\n' 'stack frame five'
printf '%s\n' 'stack frame six'
printf '%s\n' 'unrelated tail line'
} >"$log_file"
if [[ "${GH_JOB_LOG_FORMAT:-plain}" == "zip" ]]; then
base64 -d <<'ZIP'
UEsDBBQAAAAAALip/lxqPXj2qgAAAKoAAAAHAAAAam9iLmxvZ3NldHVwIGNvbXBsZXRlClRlc3QgdGltZW91dDogZXhwZWN0ZWQgcmVhZHkgc3RhdGUKc3RhY2sgZnJhbWUgb25lCnN0YWNrIGZyYW1lIHR3bwpzdGFjayBmcmFtZSB0aHJlZQpzdGFjayBmcmFtZSBmb3VyCnN0YWNrIGZyYW1lIGZpdmUKc3RhY2sgZnJhbWUgc2l4CnVucmVsYXRlZCB0YWlsIGxpbmUKUEsBAhQDFAAAAAAAuKn+XGo9ePaqAAAAqgAAAAcAAAAAAAAAAAAAAIABAAAAAGpvYi5sb2dQSwUGAAAAAAEAAQA1AAAAzwAAAAAA
ZIP
else
cat "$log_file"
fi
rm -f "$log_file"
exit 0
fi
if [[ "$*" == *"actions/workflows/opencode-code-review.yml/runs"* ]]; then
if [[ "${GH_FAIL_TRUSTED_WORKFLOW:-0}" == "1" ]]; then
echo "workflow api failed" >&2
@@ -132,6 +172,9 @@ if [[ "$1" == "pr" && "$2" == "view" && "$4" == "--json" ]]; then
"number": 123,
"headRefName": "feat/pr-state",
"headRefOid": "${GH_CHECKS_HEAD:-abc123}",
"headRepositoryOwner": { "login": "${GH_HEAD_REPOSITORY_OWNER:-kdlbs}" },
"headRepository": { "name": "${GH_HEAD_REPOSITORY_NAME:-kandev}" },
"maintainerCanModify": ${GH_MAINTAINER_CAN_MODIFY:-true},
"isCrossRepository": ${GH_CROSS_REPOSITORY:-false},
"url": $pr_url,
"comments": [
@@ -657,6 +700,7 @@ test_snapshot_happy_path() {
assert_jq "pr number" '.pr.number == 123' "$json"
assert_jq "branch" '.pr.branch == "feat/pr-state"' "$json"
assert_jq "head delivery target" '.pr.head_repository_owner == "kdlbs" and .pr.head_repository_name == "kandev" and .pr.head_ref_name == "feat/pr-state" and .pr.head_ref_oid == "abc123" and .pr.maintainer_can_modify == true' "$json"
assert_jq "since timestamp" '.since.committed_at == "2026-06-01T12:00:00Z"' "$json"
assert_jq "checks collapse duplicate workflow attempts" '.checks | length < 10' "$json"
assert_jq "latest duplicate check uses newest attempt" '[.checks[] | select(.name == "web lint")][0] | .conclusion == "success" and .run_id == "27340000001"' "$json"
@@ -1151,7 +1195,9 @@ test_summary_mode_returns_compact_fixup_state() {
json="$(<"$tmp/out.json")"
assert_jq "summary keeps pr" '.pr.number == 123' "$json"
assert_jq "summary keeps authoritative head delivery target" '.pr.head_repository_owner == "kdlbs" and .pr.head_repository_name == "kandev" and .pr.head_ref_name == "feat/pr-state" and .pr.head_ref_oid == "abc123" and .pr.maintainer_can_modify == true' "$json"
assert_jq "summary keeps since" '.since.committed_at == "2026-06-01T12:00:00Z"' "$json"
assert_jq "summary has no approval-required runs by default" '.approval_required_runs == []' "$json"
assert_jq "summary failed check count" '.failed_checks | length == 3' "$json"
assert_jq "summary failed check" '.failed_checks[] | select(.name == "e2e") | .conclusion == "failure" and .run_id == "27340000002"' "$json"
assert_jq "summary reports failed duplicate over skipped" '.failed_checks[] | select(.name == "opencode-review-fork") | .conclusion == "failure" and .run_id == "27340000007"' "$json"
@@ -1173,6 +1219,66 @@ test_summary_mode_returns_compact_fixup_state() {
pass "--summary returns compact fixup state"
}
test_summary_reports_current_head_fork_approval_runs() {
local tmp
make_tmp_dir tmp
make_mock_gh "$tmp/bin"
local json
GH_CROSS_REPOSITORY=true GH_APPROVAL_REQUIRED=1 PATH="$tmp/bin:$PATH" "$SCRIPT" --summary 123 >"$tmp/out.json"
json="$(<"$tmp/out.json")"
assert_jq "summary reports only current-head approval run" \
'.approval_required_runs | length == 1 and .[0].run_id == "444" and .[0].head_sha == "abc123" and .[0].conclusion == "action_required"' \
"$json"
pass "--summary reports current-head fork approval runs"
}
test_summary_reports_approval_run_fetch_failure() {
local tmp
make_tmp_dir tmp
make_mock_gh "$tmp/bin"
local json
GH_CROSS_REPOSITORY=true GH_FAIL_APPROVAL_RUNS=1 PATH="$tmp/bin:$PATH" "$SCRIPT" --summary 123 >"$tmp/out.json"
json="$(<"$tmp/out.json")"
assert_jq "summary reports approval-run fetch failure" \
'.approval_required_runs == [] and any(.errors[]; .source == "approval_required_runs" and .message == "workflow runs API failed")' \
"$json"
pass "--summary reports approval-run fetch failure"
}
test_job_log_mode_emits_bounded_failure_context() {
local tmp
make_tmp_dir tmp
make_mock_gh "$tmp/bin"
local output
output="$(PATH="$tmp/bin:$PATH" "$SCRIPT" --job-log 55150000002)"
if ! grep -q 'Test timeout: expected ready state' <<<"$output"; then
fail "--job-log emits matching failure context"
fi
if grep -q 'unrelated tail line' <<<"$output"; then
fail "--job-log keeps output bounded"
fi
pass "--job-log emits bounded failure context"
}
test_job_log_mode_unpacks_zip_responses() {
local tmp
make_tmp_dir tmp
make_mock_gh "$tmp/bin"
local output
output="$(GH_JOB_LOG_FORMAT=zip PATH="$tmp/bin:$PATH" "$SCRIPT" --job-log 55150000002)"
if ! grep -q 'Test timeout: expected ready state' <<<"$output"; then
fail "--job-log unpacks zip responses"
fi
pass "--job-log unpacks zip responses"
}
test_summary_all_flag_includes_historical_unresolved_threads() {
local tmp
make_tmp_dir tmp
@@ -1242,6 +1348,13 @@ test_comment_mode_rejects_incompatible_flags() {
if PATH="$tmp/bin:$PATH" "$SCRIPT" --all --comment 111 >"$tmp/out.log" 2>&1; then
fail "--comment rejects --all"
fi
if PATH="$tmp/bin:$PATH" "$SCRIPT" --comment 111 --job-log 55150000002 >"$tmp/out.log" 2>&1; then
fail "--comment rejects --job-log"
fi
if ! grep -q "scripts/pr-state --comment <comment_id>" "$tmp/out.log"; then
fail "--comment --job-log prints usage"
fi
if ! grep -q "scripts/pr-state --comment <comment_id>" "$tmp/out.log"; then
fail "--comment --all prints usage"
fi
@@ -1334,7 +1447,11 @@ test_graphql_failure_records_error_but_keeps_other_data
test_graphql_pagination_collects_all_threads
test_all_flag_includes_historical_comments_and_reviews
test_summary_mode_returns_compact_fixup_state
test_summary_reports_current_head_fork_approval_runs
test_summary_reports_approval_run_fetch_failure
test_summary_all_flag_includes_historical_unresolved_threads
test_job_log_mode_emits_bounded_failure_context
test_job_log_mode_unpacks_zip_responses
test_comment_mode_returns_full_review_comment
test_comment_mode_reports_fetch_failure
test_comment_mode_rejects_incompatible_flags