autonomous-sdlc-harness 0.4.1 → 0.5.0

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.
@@ -210,17 +210,10 @@
210
210
  # OWN. Skipped for such a record: the vanished-process reconcile, the cap
211
211
  # count, the stall watchdog, both halves of the usage gate, and the lane's
212
212
  # idle release.
213
- # * THE RELAYS — EVERY ONE A USER'S ACTION RELAYED, never a decision of this
214
- # script. The files a user's command writes into the mirror become
215
- # dispatches: a fully answered park -> `dispatch --resume answer
216
- # --answers-from <clar_dir> --indexes "<set>"` (plus `--park-loop-clear`
217
- # after a PARK_LOOP_CLEAR); a RESUME -> `dispatch --resume pause`; a PAUSE on
218
- # a `running` record -> `remote-run.sh pause`. Each is chain 0, takes no cap
219
- # slot and consults no lane; only a sent dispatch changes the record or
220
- # removes a file, so a refusal or a `gh` failure is one log line and the
221
- # next pass retries. The kill switch defers the two resumes; the pause relay
222
- # runs ahead of it, the one action that still runs under the brake, because
223
- # it starts nothing.
213
+ # * NOTHING IS RELAYED. The user's commands send pause, resume and answer to
214
+ # GitHub themselves through `remote-run.sh`, so no file in a mirror is a
215
+ # request this script acts on: the parked, paused and park-loop passes skip
216
+ # a remote record outright, sending nothing and changing nothing.
224
217
  #
225
218
  # A remote run's local working copy is a MIRROR that `remote-run.sh sync`
226
219
  # fills; it is stopped through `remote-run.sh stop`.
@@ -704,17 +697,12 @@
704
697
  # the same tick still launches under MAX_PARALLEL_RUNS=1, its
705
698
  # back-dated mirror logs draw no stall line, and a `rejected`
706
699
  # event in its stream drops no PAUSE
707
- # remote relays from that record: `parked` with question_1.md and answer_1.md
708
- # in the mirror's clarifications/feat_x/ -> ONE `-f resume=answer`
709
- # dispatch whose `answers` carries answer_1.md's bytes, the stub
710
- # NOT launched, the record `running`; `park_loop` plus
711
- # PARK_LOOP_CLEAR -> the same, with `-f park_loop_clear=true`;
712
- # `paused` with the mirror's RESUME -> ONE `-f resume=pause`
713
- # dispatch and PAUSE / RESUME / PAUSE_ACK gone; `running` with
714
- # the mirror's PAUSE -> ONE `-f action=pause` dispatch, PAUSE
715
- # gone and pause_relayed_at set. With AUTONOMOUS_STOP present
716
- # only the pause is sent; with a recorder exiting non-zero
717
- # every file and status stays
700
+ # remote passes from that record: `parked` with question_1.md and answer_1.md
701
+ # in the mirror's clarifications/feat_x/; `park_loop` plus
702
+ # PARK_LOOP_CLEAR; `paused` with the mirror's RESUME; `running`
703
+ # with the mirror's PAUSE -> each tick sends no `gh` call, does
704
+ # NOT launch the stub, and leaves every mirror file and the
705
+ # record's status as they were
718
706
  # unresolvable printf 'x' > "$d/harness.config.json"
719
707
  # -> one line on stderr, exit 1, nothing under "$d/sdlc-harness"
720
708
 
@@ -1272,7 +1260,9 @@ notify() {
1272
1260
  # bundle still says `running`, or a finished run left no
1273
1261
  # bundle, and `expired`, a registry-only value it derives
1274
1262
  # when a finished run's bundle has expired (the job can no
1275
- # longer take an answer, and the carried counts are lost);
1263
+ # longer take an answer, and the carried counts and any
1264
+ # planning drafts not yet committed that it carried are
1265
+ # lost);
1276
1266
  # `status.json` never carries either. Both map to
1277
1267
  # `paused` rather than `failed` because a `failed` record
1278
1268
  # has no resume path, while the ledger on the branch is
@@ -1295,18 +1285,14 @@ notify() {
1295
1285
  # bound of the next control poll, this job's or the next
1296
1286
  # chained one's. Set at start (see JOB MODE) and advanced
1297
1287
  # by every successful poll
1298
- # execution `github-actions` on a record launch_remote_run wrote, and
1288
+ # execution `github-actions` on a remote record — one
1289
+ # launch_remote_run wrote through
1290
+ # lib/harness-run-lib.sh's `hr_remote_record_init` — and
1299
1291
  # absent on a local one. Fixed for the run's life: a later
1300
1292
  # pass reads this field, never `execution.target`
1301
1293
  # remote_dispatched_at
1302
1294
  # the epoch second launch_remote_run's `remote-run.sh
1303
1295
  # dispatch` returned 0; empty after a failed dispatch
1304
- # park_loop_clear_pending
1305
- # `1` on a remote record clear_park_loops returned to
1306
- # `parked`, so the next answer relay sends
1307
- # `--park-loop-clear`; cleared once that relay is sent
1308
- # pause_relayed_at the epoch second the pause relay sent a remote record's
1309
- # `remote-run.sh pause`
1310
1296
  # -----------------------------------------------------------------------------
1311
1297
  # The bodies are lib/harness-run-lib.sh's THE RUN REGISTRY, shared with every
1312
1298
  # script that reads or writes this file; these wrappers bind them to $REGISTRY.
@@ -1559,7 +1545,7 @@ print_status() {
1559
1545
  }
1560
1546
 
1561
1547
  # The global kill switch — checked before every launch and at the top of every
1562
- # pass, after the remote-pause relay, which starts nothing (see REMOTE DISPATCH).
1548
+ # pass.
1563
1549
  # NEVER removed here; see the header.
1564
1550
  kill_switch_active() {
1565
1551
  [ -f "$GLOBAL_STOP" ]
@@ -2067,26 +2053,15 @@ launch_run() {
2067
2053
  # launch_remote_run <branch> <worktree> <log_path> <engine_kind>
2068
2054
  #
2069
2055
  # launch_run's bookkeeping for a remote drop (REMOTE DISPATCH in the header):
2070
- # the same fields written and cleared, plus `execution` and an empty pid, then
2056
+ # the same fields written and cleared, plus `execution` and an empty pid — the
2057
+ # one list is lib/harness-run-lib.sh's `hr_remote_record_init` — then
2071
2058
  # `remote-run.sh dispatch` in place of the window and the spawn. `running` is
2072
2059
  # written only once the dispatch returned 0, so a record never claims a run
2073
2060
  # GitHub was not asked for.
2074
2061
  launch_remote_run() {
2075
2062
  local branch="$1" worktree="$2" log_path="$3" engine_kind="$4" out rc first
2076
2063
 
2077
- registry_set "$branch" worktree "$worktree"
2078
- registry_set "$branch" log_path "$log_path"
2079
- registry_set "$branch" engine "$engine_kind"
2080
- registry_set "$branch" execution github-actions
2081
- registry_set "$branch" started_at "$(date '+%Y-%m-%dT%H:%M:%S')"
2082
- registry_set "$branch" pid ""
2083
- registry_set "$branch" remote_dispatched_at ""
2084
- registry_set "$branch" stall_restarts 0
2085
- registry_set "$branch" stall_warned ""
2086
- registry_set "$branch" stall_killing ""
2087
- registry_set "$branch" paused_by "" usage_resume_at ""
2088
- registry_set "$branch" resume_kind ""
2089
- registry_set "$branch" park_loop_cycles 0
2064
+ hr_remote_record_init "$REGISTRY" "$branch" "$worktree" "$log_path" "$engine_kind"
2090
2065
 
2091
2066
  log "dispatching '$branch' (engine=$engine_kind) to GitHub Actions via remote-run.sh (log: $log_path)"
2092
2067
  out="$(bash "$REMOTE_RUN" dispatch "$branch" --engine "$engine_kind" --resume none --chain 0 --repo "$MAIN_REPO" 2>&1)"
@@ -2228,7 +2203,7 @@ classify_run_exit() {
2228
2203
  registry_set "$branch" pause_reason "$reason" status paused
2229
2204
  log "run '$branch' paused (PAUSE honored, reason $reason) — rc=$rc"
2230
2205
  if [ "$reason" = "user" ]; then
2231
- notify paused "$branch" "$log_path" "paused as you asked — run /autonomous-sdlc-harness:branch-resume $branch to continue"
2206
+ notify paused "$branch" "$log_path" "paused as you asked — run /autonomous-sdlc-harness:branch-resume $branch to continue; $(hr_github_resume_route "$branch" "$(registry_get "$branch" engine)")"
2232
2207
  fi
2233
2208
  return 0
2234
2209
  fi
@@ -2319,7 +2294,7 @@ classify_run_exit() {
2319
2294
  registry_set "$branch" status park_loop
2320
2295
  log "run '$branch' park loop — $cycles consecutive resumes made no progress; not resuming it again (clear: $clar_dir/PARK_LOOP_CLEAR)"
2321
2296
  if [ "$JOB_MODE" = "1" ]; then
2322
- notify park_loop "$branch" "$log_path" "$cycles no-progress resumes — run /autonomous-sdlc-harness:branch-status $branch to see the question, then clear the park loop"
2297
+ notify park_loop "$branch" "$log_path" "$cycles no-progress resumes — run /autonomous-sdlc-harness:branch-status $branch to see the question, then clear the park loop; $(hr_github_answer_route "$branch" "$(registry_get "$branch" engine)" clear)"
2323
2298
  else
2324
2299
  notify park_loop "$branch" "$log_path" "$cycles no-progress resumes — create $clar_dir/PARK_LOOP_CLEAR to clear"
2325
2300
  fi
@@ -2334,7 +2309,7 @@ classify_run_exit() {
2334
2309
  registry_set "$branch" status parked
2335
2310
  log "run '$branch' parked (clarification waiting) — rc=$rc"
2336
2311
  if [ "$JOB_MODE" = "1" ]; then
2337
- notify parked "$branch" "$log_path" "answer with /autonomous-sdlc-harness:branch-answer $branch"
2312
+ notify parked "$branch" "$log_path" "answer with /autonomous-sdlc-harness:branch-answer $branch; $(hr_github_answer_route "$branch" "$(registry_get "$branch" engine)")"
2338
2313
  else
2339
2314
  notify parked "$branch" "$log_path" "See $clar_dir"
2340
2315
  fi
@@ -2419,7 +2394,8 @@ begin_park_resume() {
2419
2394
  # Returns 0 when it resumed, 1 when there was nothing to do, 10 when it deferred
2420
2395
  # for the cap and 11 when it deferred for the kill switch — the same three-way
2421
2396
  # vocabulary the inbox pass returns, so a caller that already distinguishes them
2422
- # needs no second one. A remote record returns relay_remote_answers' code.
2397
+ # needs no second one. A remote record returns 1 and logs nothing: its answers
2398
+ # reach GitHub through the user's own command (REMOTE DISPATCH).
2423
2399
  #
2424
2400
  # THE PARK IS THE UNIT. A partly answered park is not resumed, and a resume
2425
2401
  # consumes every answered pair at once: resuming on a subset would leave pairs
@@ -2433,6 +2409,7 @@ begin_park_resume() {
2433
2409
  resume_parked_run() {
2434
2410
  local branch="$1"
2435
2411
  local worktree log_path
2412
+ record_is_remote "$branch" && return 1
2436
2413
  worktree="$(registry_get "$branch" worktree)"
2437
2414
  log_path="$(registry_get "$branch" log_path)"
2438
2415
  [ -n "$worktree" ] || return 1
@@ -2467,13 +2444,6 @@ resume_parked_run() {
2467
2444
 
2468
2445
  [ -n "$log_path" ] || log_path="$LOGS_DIR/$branch.log"
2469
2446
 
2470
- # A remote run is relayed, not re-launched: no cap slot, no lane (REMOTE
2471
- # DISPATCH). The pairs stay in the mirror, which the next `sync` replaces.
2472
- if record_is_remote "$branch"; then
2473
- relay_remote_answers "$branch" "$clar_dir" "$log_path" "$answered_set"
2474
- return
2475
- fi
2476
-
2477
2447
  local current
2478
2448
  current="$(running_count)"
2479
2449
  if [ "$current" -ge "$MAX_PARALLEL_RUNS" ]; then
@@ -2503,48 +2473,6 @@ resume_parked_run() {
2503
2473
  return 0
2504
2474
  }
2505
2475
 
2506
- # remote_relay_call <log_path> <remote-run.sh argument>...
2507
- #
2508
- # One relay's `remote-run.sh` call, from the main checkout, its output appended
2509
- # to <log_path>. Returns the script's exit code (0 sent, 2 refused, 3 `gh`
2510
- # failed) and leaves its first output line in REMOTE_RELAY_FIRST for the
2511
- # caller's one log line.
2512
- remote_relay_call() {
2513
- local log_path="$1" out rc
2514
- shift
2515
- out="$(bash "$REMOTE_RUN" "$@" --repo "$MAIN_REPO" 2>&1)"
2516
- rc=$?
2517
- [ -z "$out" ] || printf '%s\n' "$out" >>"$log_path"
2518
- REMOTE_RELAY_FIRST="$(printf '%s\n' "$out" | sed -n '1p')"
2519
- return "$rc"
2520
- }
2521
-
2522
- # relay_remote_answers <branch> <clar_dir> <log_path> <answered_set>
2523
- #
2524
- # resume_parked_run's remote arm: the answered set sent as a chain-0 `--resume
2525
- # answer` dispatch, with `--park-loop-clear` while clear_park_loops has left
2526
- # `park_loop_clear_pending` set. 0 dispatched; 1 refused or `gh` failed, the
2527
- # record left `parked` and every file where it is, so the next pass retries.
2528
- relay_remote_answers() {
2529
- local branch="$1" clar_dir="$2" log_path="$3" answered_set="$4" rc
2530
- local -a plc=()
2531
- [ "$(registry_get "$branch" park_loop_clear_pending)" = "1" ] && plc=(--park-loop-clear)
2532
- remote_relay_call "$log_path" dispatch "$branch" --engine "$(registry_get "$branch" engine)" \
2533
- --resume answer --answers-from "$clar_dir" --indexes "$answered_set" ${plc[@]+"${plc[@]}"} --chain 0
2534
- rc=$?
2535
- if [ "$rc" -ne 0 ]; then
2536
- log "remote-run.sh dispatch --resume answer for '$branch' failed (exit $rc): ${REMOTE_RELAY_FIRST:-no output} — leaving it parked"
2537
- return 1
2538
- fi
2539
- log "relayed the answers ($answered_set) of parked remote run '$branch' to GitHub Actions"
2540
- registry_set "$branch" status running
2541
- registry_set "$branch" resumed_at "$(date '+%Y-%m-%dT%H:%M:%S')"
2542
- registry_set "$branch" resume_kind answer
2543
- registry_set "$branch" park_loop_clear_pending ""
2544
- notify resumed "$branch" "$log_path" "answered clarification(s) #$answered_set, dispatched"
2545
- return 0
2546
- }
2547
-
2548
2476
  # Every `parked` record, offered to the resume above. The kill switch skips the
2549
2477
  # WHOLE pass rather than each record, so an operator's brake costs one log line
2550
2478
  # in tick() instead of one per parked branch; the cap is per-record, because a
@@ -2570,14 +2498,15 @@ EOF
2570
2498
  # run's working copy; the file is removed so the clear is spent once. Answering a
2571
2499
  # question never releases the hold — only this file does. A missing working copy
2572
2500
  # or an unresolvable state directory leaves the record held. A remote record is
2573
- # also marked `park_loop_clear_pending`, because the count that matters lives in
2574
- # the job's bundle: the answer relay carries the clear to it.
2501
+ # skipped: its hold is cleared by the user's own answer or resume command, which
2502
+ # sends `--park-loop-clear` to the job (REMOTE DISPATCH).
2575
2503
  clear_park_loops() {
2576
2504
  registry_init
2577
2505
  local b worktree state_rel clear_file
2578
2506
  while IFS= read -r b; do
2579
2507
  [ -n "$b" ] || continue
2580
2508
  [ "$(registry_get "$b" status)" = "park_loop" ] || continue
2509
+ record_is_remote "$b" && continue
2581
2510
  worktree="$(registry_get "$b" worktree)"
2582
2511
  [ -n "$worktree" ] || continue
2583
2512
  [ -d "$worktree" ] || {
@@ -2593,7 +2522,6 @@ clear_park_loops() {
2593
2522
  rm -f "$clear_file"
2594
2523
  registry_set "$b" park_loop_cycles 0
2595
2524
  registry_set "$b" status parked
2596
- record_is_remote "$b" && registry_set "$b" park_loop_clear_pending 1
2597
2525
  log "park loop cleared for '$b' (PARK_LOOP_CLEAR found) — back to parked"
2598
2526
  done <<EOF
2599
2527
  $(registry_branches)
@@ -2645,12 +2573,12 @@ begin_pause_resume() {
2645
2573
  #
2646
2574
  # Resume one paused run if a RESUME trigger has landed in its working copy.
2647
2575
  # Return codes, the missing-working-copy outcome and the kill-switch/cap ordering
2648
- # are resume_parked_run's, for the same reasons. A remote record is relayed as a
2649
- # chain-0 `--resume pause` dispatch: 0 sent, 1 refused or `gh` failed with
2650
- # nothing removed.
2576
+ # are resume_parked_run's, for the same reasons. A remote record returns 1 and
2577
+ # logs nothing, as there.
2651
2578
  resume_paused_run() {
2652
2579
  local branch="$1"
2653
2580
  local worktree log_path
2581
+ record_is_remote "$branch" && return 1
2654
2582
  worktree="$(registry_get "$branch" worktree)"
2655
2583
  log_path="$(registry_get "$branch" log_path)"
2656
2584
  [ -n "$worktree" ] || return 1
@@ -2681,23 +2609,6 @@ resume_paused_run() {
2681
2609
 
2682
2610
  [ -n "$log_path" ] || log_path="$LOGS_DIR/$branch.log"
2683
2611
 
2684
- # A remote run is relayed, not re-launched: no cap slot, no lane (REMOTE
2685
- # DISPATCH). The sentinels go only once the dispatch was sent.
2686
- if record_is_remote "$branch"; then
2687
- local rc
2688
- remote_relay_call "$log_path" dispatch "$branch" --engine "$(registry_get "$branch" engine)" \
2689
- --resume pause --chain 0
2690
- rc=$?
2691
- if [ "$rc" -ne 0 ]; then
2692
- log "remote-run.sh dispatch --resume pause for '$branch' failed (exit $rc): ${REMOTE_RELAY_FIRST:-no output} — leaving it paused"
2693
- return 1
2694
- fi
2695
- log "relayed the RESUME of paused remote run '$branch' to GitHub Actions"
2696
- begin_pause_resume "$branch" "$state_abs"
2697
- notify resumed "$branch" "$log_path" "after pause, dispatched"
2698
- return 0
2699
- fi
2700
-
2701
2612
  local current
2702
2613
  current="$(running_count)"
2703
2614
  if [ "$current" -ge "$MAX_PARALLEL_RUNS" ]; then
@@ -2740,39 +2651,6 @@ $(registry_branches)
2740
2651
  EOF
2741
2652
  }
2742
2653
 
2743
- # THE PAUSE RELAY (REMOTE DISPATCH). A `running` remote record whose mirror
2744
- # holds `<state_dir>/PAUSE` — a user's pause — is sent `remote-run.sh pause`;
2745
- # on exit 0 the mirror's PAUSE goes and `pause_relayed_at` is stamped, and on
2746
- # any other exit both stay for the next pass. Called from `tick` ahead of the
2747
- # kill switch, because a pause starts nothing; it consults neither the cap nor
2748
- # the lane.
2749
- relay_remote_pauses() {
2750
- registry_init
2751
- local b wt state_rel rc log_path
2752
- while IFS= read -r b; do
2753
- [ -n "$b" ] || continue
2754
- record_is_remote "$b" || continue
2755
- [ "$(registry_get "$b" status)" = "running" ] || continue
2756
- wt="$(registry_get "$b" worktree)"
2757
- [ -n "$wt" ] && [ -d "$wt" ] || continue
2758
- state_rel="$(run_state_dir "$wt")" || continue
2759
- [ -f "$wt/$state_rel/PAUSE" ] || continue
2760
- log_path="$(registry_get "$b" log_path)"
2761
- [ -n "$log_path" ] || log_path="$LOGS_DIR/$b.log"
2762
- remote_relay_call "$log_path" pause "$b"
2763
- rc=$?
2764
- if [ "$rc" -ne 0 ]; then
2765
- log "remote-run.sh pause for '$b' failed (exit $rc): ${REMOTE_RELAY_FIRST:-no output} — PAUSE left in the mirror"
2766
- continue
2767
- fi
2768
- rm -f "$wt/$state_rel/PAUSE"
2769
- registry_set "$b" pause_relayed_at "$(date +%s)"
2770
- log "relayed the PAUSE of remote run '$b' to GitHub Actions"
2771
- done <<EOF
2772
- $(registry_branches)
2773
- EOF
2774
- }
2775
-
2776
2654
  # -----------------------------------------------------------------------------
2777
2655
  # THE STALENESS WATCHDOG. The header states what this pass heals that the
2778
2656
  # reconcile pass cannot see, why a stale mtime ALONE never kills, why the
@@ -3021,29 +2899,27 @@ reject_preserving_status() {
3021
2899
  # remote_commit_and_push <branch> <worktree> <log_path> <file> <fname> <dest> <rel> <subject> <what>
3022
2900
  #
3023
2901
  # The remote arm's commit-and-push, BLOCKING (REMOTE DISPATCH in the header):
3024
- # 0 = <dest> is committed and origin/<branch> carries HEAD; 1 = it is not, and
2902
+ # 0 = <rel> is committed and origin/<branch> carries HEAD; 1 = it is not, and
3025
2903
  # fail_before_launch has already run, so the caller dispatches nothing. The
3026
- # same staging, identical-re-drop skip and wrappers the local arm uses; the
3027
- # push stays a separate statement from the commit.
2904
+ # library's staging, identical-re-drop skip and landed test (THE ARTIFACT
2905
+ # PLACEMENT in lib/harness-run-lib.sh) with the local arm's wrappers; the push
2906
+ # stays a separate statement from the commit. <dest> is the caller's.
3028
2907
  remote_commit_and_push() {
3029
2908
  local branch="$1" worktree="$2" log_path="$3" file="$4" fname="$5"
3030
- local dest="$6" rel="$7" subject="$8" what="$9" head upstream
2909
+ local dest="$6" rel="$7" subject="$8" what="$9" rc=0
3031
2910
 
3032
- git -C "$worktree" add "$dest"
3033
- if git -C "$worktree" diff --cached --quiet "$dest"; then
3034
- log "the $what for '$branch' is already committed (identical re-drop) — skipping the commit, pushing anyway"
3035
- elif "$COMMIT_ON_BRANCH" --repo "$worktree" "$rel" -- "$subject" >>"$log_path" 2>&1; then
3036
- log "committed the $what for '$branch' ($subject)"
3037
- else
3038
- log "commit-on-branch.sh could not commit the $what for '$branch' — not dispatching (see $log_path)"
3039
- fail_before_launch "$branch" "$file" "$fname" "(commit-on-branch.sh could not commit the $what — nothing dispatched; see $log_path)"
3040
- return 1
3041
- fi
2911
+ hr_commit_placed "$COMMIT_ON_BRANCH" "$worktree" "$rel" "$subject" >>"$log_path" 2>&1 || rc=$?
2912
+ case "$rc" in
2913
+ 3) log "the $what for '$branch' is already committed (identical re-drop) — skipping the commit, pushing anyway" ;;
2914
+ 0) log "committed the $what for '$branch' ($subject)" ;;
2915
+ *)
2916
+ log "commit-on-branch.sh could not commit the $what for '$branch' — not dispatching (see $log_path)"
2917
+ fail_before_launch "$branch" "$file" "$fname" "(commit-on-branch.sh could not commit the $what — nothing dispatched; see $log_path)"
2918
+ return 1
2919
+ ;;
2920
+ esac
3042
2921
 
3043
- "$PUSH_BRANCH" "$worktree" >>"$log_path" 2>&1
3044
- head="$(git -C "$worktree" rev-parse --verify --quiet HEAD)" || head=""
3045
- upstream="$(git -C "$worktree" rev-parse --verify --quiet "refs/remotes/origin/$branch")" || upstream=""
3046
- if [ -z "$head" ] || [ "$head" != "$upstream" ]; then
2922
+ if ! hr_push_landed "$PUSH_BRANCH" "$worktree" "$branch" >>"$log_path" 2>&1; then
3047
2923
  log "push-branch.sh did not bring origin/$branch to HEAD after the $what for '$branch' — not dispatching (see $log_path)"
3048
2924
  fail_before_launch "$branch" "$file" "$fname" "(push-branch.sh did not push the $what to origin/$branch — nothing dispatched; the job checks out origin/$branch; see $log_path)"
3049
2925
  return 1
@@ -3065,18 +2941,8 @@ remote_commit_and_push() {
3065
2941
  # ^(.+)_review(_[0-9]+)?\.md$ -> user_review engine, reuse-else-recreate
3066
2942
  # ^(.+)_docs\.md$ -> docs engine, a fresh working copy
3067
2943
  #
3068
- # The task-prompt pattern is tested FIRST (the more specific suffix), but the
3069
- # anchored SUFFIX regexes are mutually exclusive by construction: a filename
3070
- # cannot end in more than one of `_task_prompt.md` / `_review[_<n>].md` /
3071
- # `_docs.md`, so a branch whose own name contains `review` or `task_prompt`
3072
- # cannot be mis-routed — `foo_review_task_prompt.md` is the task engine on branch
3073
- # `foo_review`, and `foo_task_prompt_review.md` is the review engine on branch
3074
- # `foo_task_prompt`. POSIX leftmost-longest matching of the greedy `(.+)` derives
3075
- # the right branch from a round-suffixed name: `foo_review_2.md` -> branch `foo`
3076
- # (the `_2` is consumed by the optional `(_[0-9]+)?`), while
3077
- # `foo_review_2_review.md` -> branch `foo_review_2`. THE WATCHER DERIVES ONLY THE
3078
- # BRANCH, never the round: the engine resolves the latest round itself, inside
3079
- # the working copy, which is why nothing here has to remember one.
2944
+ # The library owns these patterns and their order: `hr_inbox_route_var` in
2945
+ # lib/harness-run-lib.sh, whose comment states why they cannot mis-route.
3080
2946
  #
3081
2947
  # A filename matching none of the three is logged and ARCHIVED rather than left
3082
2948
  # where it is, so it is not re-logged on every pass for as long as the watcher
@@ -3089,24 +2955,13 @@ process_inbox_file() {
3089
2955
 
3090
2956
  # (1) Route the filename to its pairing and derive <branch> — see above.
3091
2957
  local branch engine_kind
3092
- branch="$(printf '%s' "$fname" | sed -nE 's/^(.+)_task_prompt\.md$/\1/p')"
3093
- if [ -n "$branch" ]; then
3094
- engine_kind="task"
3095
- else
3096
- branch="$(printf '%s' "$fname" | sed -nE 's/^(.+)_review(_[0-9]+)?\.md$/\1/p')"
3097
- if [ -n "$branch" ]; then
3098
- engine_kind="user_review"
3099
- else
3100
- branch="$(printf '%s' "$fname" | sed -nE 's/^(.+)_docs\.md$/\1/p')"
3101
- if [ -n "$branch" ]; then
3102
- engine_kind="docs"
3103
- else
3104
- log "rejecting '$fname': not a <branch>_task_prompt.md / <branch>_review[_<n>].md / <branch>_docs.md file — skipping"
3105
- mv "$file" "$ARCHIVE_DIR/rejected_$(date '+%Y%m%d%H%M%S')_$fname" 2>/dev/null || rm -f "$file"
3106
- return 0
3107
- fi
3108
- fi
2958
+ if ! hr_inbox_route_var "$fname"; then
2959
+ log "rejecting '$fname': not a <branch>_task_prompt.md / <branch>_review[_<n>].md / <branch>_docs.md file — skipping"
2960
+ mv "$file" "$ARCHIVE_DIR/rejected_$(date '+%Y%m%d%H%M%S')_$fname" 2>/dev/null || rm -f "$file"
2961
+ return 0
3109
2962
  fi
2963
+ branch="$HR_INBOX_BRANCH"
2964
+ engine_kind="$HR_INBOX_KIND"
3110
2965
 
3111
2966
  # The central log every step below appends to. A branch derived from a FILENAME
3112
2967
  # cannot contain a `/`, so this name needs no sanitizing — unlike the
@@ -3276,10 +3131,11 @@ process_inbox_file() {
3276
3131
 
3277
3132
  # (3a) Copy the dropped prompt into the working copy, then archive the inbox
3278
3133
  # file so it is not processed again.
3279
- local prompt_rel="$state_rel/task_prompts/${branch}_task_prompt.md"
3280
- local prompt_dest="$worktree/$prompt_rel"
3281
- mkdir -p "$worktree/$state_rel/task_prompts"
3282
- cp "$file" "$prompt_dest"
3134
+ local prompt_rel prompt_dest prompt_subject prompt_rc=0
3135
+ prompt_rel="$(hr_task_prompt_rel "$state_rel" "$branch")"
3136
+ prompt_dest="$worktree/$prompt_rel"
3137
+ prompt_subject="$(hr_task_prompt_subject "$branch")"
3138
+ hr_place_artifact "$worktree" "$file" "$prompt_rel"
3283
3139
  mv "$file" "$ARCHIVE_DIR/$(date '+%Y%m%d%H%M%S')_$fname" 2>/dev/null || rm -f "$file"
3284
3140
  log "copied the prompt -> $prompt_dest; archived the inbox file"
3285
3141
 
@@ -3294,13 +3150,14 @@ process_inbox_file() {
3294
3150
  #
3295
3151
  # Only the prompt is staged, by explicit path — never `git add -A` or
3296
3152
  # `git add .`, matching the no-blanket-add rule every unattended commit point
3297
- # in this family follows. The `diff --cached --quiet` pre-check is what makes
3153
+ # in this family follows. The nothing-staged pre-check is what makes
3298
3154
  # an identical re-drop of an already-committed prompt a no-op instead of an
3299
3155
  # empty commit; when there IS a diff, the WRAPPER does the real staging and
3300
3156
  # the commit, so this commit point inherits its protected-branch refusal
3301
3157
  # rather than re-implementing it. The wrapper stages paths RELATIVE TO THE
3302
- # REPOSITORY TOP, so it is handed the repo-relative path; the absolute one is
3303
- # `git -C "$worktree"`-scoped and only feeds the skip pre-check.
3158
+ # REPOSITORY TOP, so it is handed the repo-relative path. The placement
3159
+ # itself lives in lib/harness-run-lib.sh (THE ARTIFACT PLACEMENT), shared
3160
+ # with a job that starts a run.
3304
3161
  #
3305
3162
  # A FAILURE AT EITHER STEP IS LOGGED AND THE RUN LAUNCHES ANYWAY: a prompt
3306
3163
  # commit that did not land has to be VISIBLE, and it must never be the reason
@@ -3311,20 +3168,18 @@ process_inbox_file() {
3311
3168
  # dispatch instead (REMOTE DISPATCH in the header).
3312
3169
  if [ "$remote" = 1 ]; then
3313
3170
  remote_commit_and_push "$branch" "$worktree" "$log_path" "$file" "$fname" \
3314
- "$prompt_dest" "$prompt_rel" "chore: add task prompt for $branch" "task prompt" || return 0
3171
+ "$prompt_dest" "$prompt_rel" "$prompt_subject" "task prompt" || return 0
3315
3172
  else
3316
- git -C "$worktree" add "$prompt_dest"
3317
- if git -C "$worktree" diff --cached --quiet "$prompt_dest"; then
3318
- log "the task prompt for '$branch' is already committed (identical re-drop) — skipping the commit"
3319
- elif "$COMMIT_ON_BRANCH" --repo "$worktree" \
3320
- "$prompt_rel" \
3321
- -- "chore: add task prompt for $branch" >>"$log_path" 2>&1; then
3322
- log "committed the task prompt for '$branch' (chore: add task prompt for $branch)"
3323
- "$PUSH_BRANCH" "$worktree" >>"$log_path" 2>&1 ||
3324
- log "WARNING: push-branch.sh failed after the task-prompt commit for '$branch' — continuing"
3325
- else
3326
- log "WARNING: could not commit the task prompt for '$branch' — launching anyway (its working-tree-clean precondition may be dishonest; see $log_path)"
3327
- fi
3173
+ hr_commit_placed "$COMMIT_ON_BRANCH" "$worktree" "$prompt_rel" "$prompt_subject" >>"$log_path" 2>&1 || prompt_rc=$?
3174
+ case "$prompt_rc" in
3175
+ 3) log "the task prompt for '$branch' is already committed (identical re-drop) — skipping the commit" ;;
3176
+ 0)
3177
+ log "committed the task prompt for '$branch' ($prompt_subject)"
3178
+ "$PUSH_BRANCH" "$worktree" >>"$log_path" 2>&1 ||
3179
+ log "WARNING: push-branch.sh failed after the task-prompt commit for '$branch' — continuing"
3180
+ ;;
3181
+ *) log "WARNING: could not commit the task prompt for '$branch' — launching anyway (its working-tree-clean precondition may be dishonest; see $log_path)" ;;
3182
+ esac
3328
3183
  fi
3329
3184
  elif [ "$engine_kind" = "docs" ]; then
3330
3185
  # (2c) Docs path: the task path's strategy exactly — a FRESH working copy off
@@ -3352,26 +3207,24 @@ process_inbox_file() {
3352
3207
  # rationale is stated once, above.
3353
3208
  local docs_rel="$state_rel/docs_catalog/${branch}_docs.md"
3354
3209
  local docs_dest="$worktree/$docs_rel"
3355
- mkdir -p "$worktree/$state_rel/docs_catalog"
3356
- cp "$file" "$docs_dest"
3210
+ local docs_subject="chore: add docs checklist for $branch" docs_rc=0
3211
+ hr_place_artifact "$worktree" "$file" "$docs_rel"
3357
3212
  mv "$file" "$ARCHIVE_DIR/$(date '+%Y%m%d%H%M%S')_$fname" 2>/dev/null || rm -f "$file"
3358
3213
  log "copied the docs checklist -> $docs_dest; archived the inbox file"
3359
3214
  if [ "$remote" = 1 ]; then
3360
3215
  remote_commit_and_push "$branch" "$worktree" "$log_path" "$file" "$fname" \
3361
- "$docs_dest" "$docs_rel" "chore: add docs checklist for $branch" "docs checklist" || return 0
3216
+ "$docs_dest" "$docs_rel" "$docs_subject" "docs checklist" || return 0
3362
3217
  else
3363
- git -C "$worktree" add "$docs_dest"
3364
- if git -C "$worktree" diff --cached --quiet "$docs_dest"; then
3365
- log "the docs checklist for '$branch' is already committed (identical re-drop) — skipping the commit"
3366
- elif "$COMMIT_ON_BRANCH" --repo "$worktree" \
3367
- "$docs_rel" \
3368
- -- "chore: add docs checklist for $branch" >>"$log_path" 2>&1; then
3369
- log "committed the docs checklist for '$branch' (chore: add docs checklist for $branch)"
3370
- "$PUSH_BRANCH" "$worktree" >>"$log_path" 2>&1 ||
3371
- log "WARNING: push-branch.sh failed after the docs-checklist commit for '$branch' — continuing"
3372
- else
3373
- log "WARNING: could not commit the docs checklist for '$branch' — launching anyway (see $log_path)"
3374
- fi
3218
+ hr_commit_placed "$COMMIT_ON_BRANCH" "$worktree" "$docs_rel" "$docs_subject" >>"$log_path" 2>&1 || docs_rc=$?
3219
+ case "$docs_rc" in
3220
+ 3) log "the docs checklist for '$branch' is already committed (identical re-drop) — skipping the commit" ;;
3221
+ 0)
3222
+ log "committed the docs checklist for '$branch' ($docs_subject)"
3223
+ "$PUSH_BRANCH" "$worktree" >>"$log_path" 2>&1 ||
3224
+ log "WARNING: push-branch.sh failed after the docs-checklist commit for '$branch' — continuing"
3225
+ ;;
3226
+ *) log "WARNING: could not commit the docs checklist for '$branch' — launching anyway (see $log_path)" ;;
3227
+ esac
3375
3228
  fi
3376
3229
  else
3377
3230
  # (2b) Review path: REUSE the branch's existing working copy when it is
@@ -3464,7 +3317,7 @@ process_inbox_file() {
3464
3317
  log "copied the review -> $review_dest; archived the inbox file"
3465
3318
  if [ "$remote" = 1 ]; then
3466
3319
  remote_commit_and_push "$branch" "$worktree" "$log_path" "$file" "$fname" \
3467
- "$review_dest" "$review_rel" "chore: add user review for $branch" "user review" || return 0
3320
+ "$review_dest" "$review_rel" "$(hr_user_review_subject "$branch")" "user review" || return 0
3468
3321
  fi
3469
3322
  fi
3470
3323
 
@@ -3733,8 +3586,7 @@ usage_gate() {
3733
3586
  local b status pb ra wt lp state_rel="" state_abs=""
3734
3587
  while IFS= read -r b; do
3735
3588
  [ -n "$b" ] || continue
3736
- # The auto-resume side skips a remote run: its job gates itself, and
3737
- # resume_paused_run relays only a user's RESUME.
3589
+ # The auto-resume side skips a remote run: its job gates itself.
3738
3590
  record_is_remote "$b" && continue
3739
3591
  status="$(registry_get "$b" status)"
3740
3592
  pb="$(registry_get "$b" paused_by)"
@@ -3919,11 +3771,8 @@ EOF
3919
3771
  }
3920
3772
 
3921
3773
  # -----------------------------------------------------------------------------
3922
- # One pass. The relay of a remote run's pause first, then the kill switch, so an
3923
- # operator's brake beats everything else — relaying a remote run's pause is the
3924
- # one action that still runs under the brake, because it starts nothing (see
3925
- # REMOTE DISPATCH) — then the reconcile that frees capacity for the passes that
3926
- # read the cap.
3774
+ # One pass. The kill switch first, so an operator's brake beats everything
3775
+ # else, then the reconcile that frees capacity for the passes that read the cap.
3927
3776
  # -----------------------------------------------------------------------------
3928
3777
  tick() {
3929
3778
  # Drop the library's per-process cache so an edit to `harness.config.json` is
@@ -3933,8 +3782,6 @@ tick() {
3933
3782
  hr_config_reset
3934
3783
  hr_config_load "$MAIN_REPO" || :
3935
3784
 
3936
- relay_remote_pauses
3937
-
3938
3785
  if kill_switch_active; then
3939
3786
  log "global kill switch active — not launching or resuming runs this pass"
3940
3787
  # A braked watcher must not sit on the machine-level lane: it is starting
@@ -4340,14 +4187,14 @@ run_job() {
4340
4187
  # run over rather than waiting on nothing.
4341
4188
  decision=wait-poller
4342
4189
  detail="usage limit reached; the in-job usage wait passed its bound (REMOTE_WAIT_MAX_SECS=${REMOTE_WAIT_MAX_SECS}s past the reset) without a resume"
4343
- notify paused "$branch" "$log_path" "usage limit: the in-job wait passed ${REMOTE_WAIT_MAX_SECS}s after the reset without a resume — run /autonomous-sdlc-harness:branch-resume $branch to continue"
4190
+ notify paused "$branch" "$log_path" "usage limit: the in-job wait passed ${REMOTE_WAIT_MAX_SECS}s after the reset without a resume — run /autonomous-sdlc-harness:branch-resume $branch to continue; $(hr_github_resume_route "$branch" "$(registry_get "$branch" engine)")"
4344
4191
  break
4345
4192
  fi
4346
4193
  ;;
4347
4194
  *)
4348
4195
  job_auto_resume "$branch" "$worktree" "$log_path" "$state_abs" "an overload self-pause" && continue
4349
4196
  detail="paused itself on API overload; automatic resumes exhausted"
4350
- notify paused "$branch" "$log_path" "paused itself on API overload — run /autonomous-sdlc-harness:branch-resume $branch to continue"
4197
+ notify paused "$branch" "$log_path" "paused itself on API overload — run /autonomous-sdlc-harness:branch-resume $branch to continue; $(hr_github_resume_route "$branch" "$(registry_get "$branch" engine)")"
4351
4198
  break
4352
4199
  ;;
4353
4200
  esac