autonomous-sdlc-harness 0.4.2 → 0.6.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.
Files changed (41) hide show
  1. package/dist/commands/init.js +113 -16
  2. package/dist/commands/init.js.map +1 -1
  3. package/dist/config/check.js +28 -6
  4. package/dist/config/check.js.map +1 -1
  5. package/dist/config/model.js +64 -5
  6. package/dist/config/model.js.map +1 -1
  7. package/dist/core/pluginIdentity.js +2 -0
  8. package/dist/core/pluginIdentity.js.map +1 -1
  9. package/dist/core/writer.js +10 -5
  10. package/dist/core/writer.js.map +1 -1
  11. package/dist/core/yamlScalar.js +14 -0
  12. package/dist/core/yamlScalar.js.map +1 -0
  13. package/dist/doctor/checks.js +454 -23
  14. package/dist/doctor/checks.js.map +1 -1
  15. package/dist/generators/githubWorkflows.js +125 -20
  16. package/dist/generators/githubWorkflows.js.map +1 -1
  17. package/dist/generators/repoRoot.js +17 -8
  18. package/dist/generators/repoRoot.js.map +1 -1
  19. package/dist/remote/githubActions.js +110 -7
  20. package/dist/remote/githubActions.js.map +1 -1
  21. package/dist/retrieval/pythonBackend.js +114 -0
  22. package/dist/retrieval/pythonBackend.js.map +1 -0
  23. package/dist/retrieval/setup.js +8 -0
  24. package/dist/retrieval/setup.js.map +1 -1
  25. package/package.json +1 -1
  26. package/templates/README.md +1 -1
  27. package/templates/github/workflows/harness-control.yml +184 -0
  28. package/templates/github/workflows/harness-resume.yml +9 -0
  29. package/templates/github/workflows/harness-run.yml +127 -12
  30. package/templates/github/workflows/harness-trigger.yml +144 -0
  31. package/templates/repo/gitignore +5 -0
  32. package/templates/scripts/README.md +1 -1
  33. package/templates/scripts/autonomous-watcher.sh +121 -249
  34. package/templates/scripts/create-worktree.sh +52 -10
  35. package/templates/scripts/docs-search-server.sh +88 -17
  36. package/templates/scripts/lib/harness-run-lib.sh +614 -14
  37. package/templates/scripts/remote-run.sh +3684 -144
  38. package/templates/scripts/scratch-run.sh +54 -73
  39. package/templates/state-dir/README-root.md +1 -1
  40. package/templates/state-dir/scratch/README.md +4 -2
  41. package/templates/state-dir/user_reviews/README.md +2 -2
@@ -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`.
@@ -383,7 +376,12 @@
383
376
  # and the next job no `resumed` for it: a chained continuation is not an
384
377
  # event the user acts on. classify_run_exit's `paused` arm notifies only a
385
378
  # `user` pause; job mode sends the others once it has decided. Their titles
386
- # carry HARNESS_REMOTE_SLUG when set.
379
+ # carry HARNESS_REMOTE_SLUG when set. Each job-mode event except `failed` is
380
+ # also passed to `remote-run.sh report` as it happens, and `failed` once,
381
+ # when the job ends `failed`; `report` comments on the run's pull request or
382
+ # issue and moves its state label when `forge` is `github`; `autonomous-notify.sh`
383
+ # is unchanged, and `completed` is reported by the workflow's `deliver` step
384
+ # after the push.
387
385
  # * THE INTERACTIVE-TEST PHASE IS SKIPPED, not run: a runner has no browser
388
386
  # wiring, application dependencies or QA credentials for it. With
389
387
  # `phases.qa` true, the task and user_review launch prompts gain one clause
@@ -704,17 +702,12 @@
704
702
  # the same tick still launches under MAX_PARALLEL_RUNS=1, its
705
703
  # back-dated mirror logs draw no stall line, and a `rejected`
706
704
  # 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
705
+ # remote passes from that record: `parked` with question_1.md and answer_1.md
706
+ # in the mirror's clarifications/feat_x/; `park_loop` plus
707
+ # PARK_LOOP_CLEAR; `paused` with the mirror's RESUME; `running`
708
+ # with the mirror's PAUSE -> each tick sends no `gh` call, does
709
+ # NOT launch the stub, and leaves every mirror file and the
710
+ # record's status as they were
718
711
  # unresolvable printf 'x' > "$d/harness.config.json"
719
712
  # -> one line on stderr, exit 1, nothing under "$d/sdlc-harness"
720
713
 
@@ -1162,12 +1155,33 @@ unset USAGE_LANE_ENABLED_RETIRED
1162
1155
  # Every lifecycle event goes out through here, so a notifier that is missing or
1163
1156
  # not executable costs one log line instead of ending a pass. Best-effort by
1164
1157
  # contract: the notifier itself never fails its caller.
1158
+ # JOB MODE ONLY: the event is also passed to `remote-run.sh report` via
1159
+ # job_report. `failed` is not reported here: run_job reports it once its
1160
+ # automatic resumes are ruled out.
1165
1161
  notify() {
1166
1162
  if [ ! -x "$NOTIFY" ]; then
1167
1163
  log "notify: '$NOTIFY' is not executable — '${1:-?}' event for '${2:-?}' not delivered"
1164
+ else
1165
+ "$NOTIFY" "$@" || true
1166
+ fi
1167
+ [ "$JOB_MODE" = 1 ] || return 0
1168
+ [ "${1:-}" != "failed" ] || return 0
1169
+ job_report "$1" "$2" "${3:-}"
1170
+ }
1171
+
1172
+ # job_report <event> <branch> [<log>] — the only route to GitHub. The detail is
1173
+ # withheld: it names local slash commands, and `report` words its comment from
1174
+ # the registry record itself.
1175
+ job_report() {
1176
+ if [ ! -r "$REMOTE_RUN" ]; then
1177
+ log "notify: '$REMOTE_RUN' is not readable — '${1:-?}' event for '${2:-?}' not reported on GitHub"
1168
1178
  return 0
1169
1179
  fi
1170
- "$NOTIFY" "$@" || true
1180
+ if [ -n "${3:-}" ]; then
1181
+ bash "$REMOTE_RUN" report "$1" "$2" --repo "$MAIN_REPO" >>"$3" 2>&1 || true
1182
+ else
1183
+ bash "$REMOTE_RUN" report "$1" "$2" --repo "$MAIN_REPO" >/dev/null 2>&1 || true
1184
+ fi
1171
1185
  }
1172
1186
 
1173
1187
  # -----------------------------------------------------------------------------
@@ -1297,18 +1311,14 @@ notify() {
1297
1311
  # bound of the next control poll, this job's or the next
1298
1312
  # chained one's. Set at start (see JOB MODE) and advanced
1299
1313
  # by every successful poll
1300
- # execution `github-actions` on a record launch_remote_run wrote, and
1314
+ # execution `github-actions` on a remote record — one
1315
+ # launch_remote_run wrote through
1316
+ # lib/harness-run-lib.sh's `hr_remote_record_init` — and
1301
1317
  # absent on a local one. Fixed for the run's life: a later
1302
1318
  # pass reads this field, never `execution.target`
1303
1319
  # remote_dispatched_at
1304
1320
  # the epoch second launch_remote_run's `remote-run.sh
1305
1321
  # dispatch` returned 0; empty after a failed dispatch
1306
- # park_loop_clear_pending
1307
- # `1` on a remote record clear_park_loops returned to
1308
- # `parked`, so the next answer relay sends
1309
- # `--park-loop-clear`; cleared once that relay is sent
1310
- # pause_relayed_at the epoch second the pause relay sent a remote record's
1311
- # `remote-run.sh pause`
1312
1322
  # -----------------------------------------------------------------------------
1313
1323
  # The bodies are lib/harness-run-lib.sh's THE RUN REGISTRY, shared with every
1314
1324
  # script that reads or writes this file; these wrappers bind them to $REGISTRY.
@@ -1561,7 +1571,7 @@ print_status() {
1561
1571
  }
1562
1572
 
1563
1573
  # The global kill switch — checked before every launch and at the top of every
1564
- # pass, after the remote-pause relay, which starts nothing (see REMOTE DISPATCH).
1574
+ # pass.
1565
1575
  # NEVER removed here; see the header.
1566
1576
  kill_switch_active() {
1567
1577
  [ -f "$GLOBAL_STOP" ]
@@ -2069,26 +2079,15 @@ launch_run() {
2069
2079
  # launch_remote_run <branch> <worktree> <log_path> <engine_kind>
2070
2080
  #
2071
2081
  # launch_run's bookkeeping for a remote drop (REMOTE DISPATCH in the header):
2072
- # the same fields written and cleared, plus `execution` and an empty pid, then
2082
+ # the same fields written and cleared, plus `execution` and an empty pid — the
2083
+ # one list is lib/harness-run-lib.sh's `hr_remote_record_init` — then
2073
2084
  # `remote-run.sh dispatch` in place of the window and the spawn. `running` is
2074
2085
  # written only once the dispatch returned 0, so a record never claims a run
2075
2086
  # GitHub was not asked for.
2076
2087
  launch_remote_run() {
2077
2088
  local branch="$1" worktree="$2" log_path="$3" engine_kind="$4" out rc first
2078
2089
 
2079
- registry_set "$branch" worktree "$worktree"
2080
- registry_set "$branch" log_path "$log_path"
2081
- registry_set "$branch" engine "$engine_kind"
2082
- registry_set "$branch" execution github-actions
2083
- registry_set "$branch" started_at "$(date '+%Y-%m-%dT%H:%M:%S')"
2084
- registry_set "$branch" pid ""
2085
- registry_set "$branch" remote_dispatched_at ""
2086
- registry_set "$branch" stall_restarts 0
2087
- registry_set "$branch" stall_warned ""
2088
- registry_set "$branch" stall_killing ""
2089
- registry_set "$branch" paused_by "" usage_resume_at ""
2090
- registry_set "$branch" resume_kind ""
2091
- registry_set "$branch" park_loop_cycles 0
2090
+ hr_remote_record_init "$REGISTRY" "$branch" "$worktree" "$log_path" "$engine_kind"
2092
2091
 
2093
2092
  log "dispatching '$branch' (engine=$engine_kind) to GitHub Actions via remote-run.sh (log: $log_path)"
2094
2093
  out="$(bash "$REMOTE_RUN" dispatch "$branch" --engine "$engine_kind" --resume none --chain 0 --repo "$MAIN_REPO" 2>&1)"
@@ -2230,7 +2229,7 @@ classify_run_exit() {
2230
2229
  registry_set "$branch" pause_reason "$reason" status paused
2231
2230
  log "run '$branch' paused (PAUSE honored, reason $reason) — rc=$rc"
2232
2231
  if [ "$reason" = "user" ]; then
2233
- notify paused "$branch" "$log_path" "paused as you asked — run /autonomous-sdlc-harness:branch-resume $branch to continue"
2232
+ 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)")"
2234
2233
  fi
2235
2234
  return 0
2236
2235
  fi
@@ -2321,7 +2320,7 @@ classify_run_exit() {
2321
2320
  registry_set "$branch" status park_loop
2322
2321
  log "run '$branch' park loop — $cycles consecutive resumes made no progress; not resuming it again (clear: $clar_dir/PARK_LOOP_CLEAR)"
2323
2322
  if [ "$JOB_MODE" = "1" ]; then
2324
- 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"
2323
+ 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)"
2325
2324
  else
2326
2325
  notify park_loop "$branch" "$log_path" "$cycles no-progress resumes — create $clar_dir/PARK_LOOP_CLEAR to clear"
2327
2326
  fi
@@ -2336,7 +2335,7 @@ classify_run_exit() {
2336
2335
  registry_set "$branch" status parked
2337
2336
  log "run '$branch' parked (clarification waiting) — rc=$rc"
2338
2337
  if [ "$JOB_MODE" = "1" ]; then
2339
- notify parked "$branch" "$log_path" "answer with /autonomous-sdlc-harness:branch-answer $branch"
2338
+ notify parked "$branch" "$log_path" "answer with /autonomous-sdlc-harness:branch-answer $branch; $(hr_github_answer_route "$branch" "$(registry_get "$branch" engine)")"
2340
2339
  else
2341
2340
  notify parked "$branch" "$log_path" "See $clar_dir"
2342
2341
  fi
@@ -2421,7 +2420,8 @@ begin_park_resume() {
2421
2420
  # Returns 0 when it resumed, 1 when there was nothing to do, 10 when it deferred
2422
2421
  # for the cap and 11 when it deferred for the kill switch — the same three-way
2423
2422
  # vocabulary the inbox pass returns, so a caller that already distinguishes them
2424
- # needs no second one. A remote record returns relay_remote_answers' code.
2423
+ # needs no second one. A remote record returns 1 and logs nothing: its answers
2424
+ # reach GitHub through the user's own command (REMOTE DISPATCH).
2425
2425
  #
2426
2426
  # THE PARK IS THE UNIT. A partly answered park is not resumed, and a resume
2427
2427
  # consumes every answered pair at once: resuming on a subset would leave pairs
@@ -2435,6 +2435,7 @@ begin_park_resume() {
2435
2435
  resume_parked_run() {
2436
2436
  local branch="$1"
2437
2437
  local worktree log_path
2438
+ record_is_remote "$branch" && return 1
2438
2439
  worktree="$(registry_get "$branch" worktree)"
2439
2440
  log_path="$(registry_get "$branch" log_path)"
2440
2441
  [ -n "$worktree" ] || return 1
@@ -2469,13 +2470,6 @@ resume_parked_run() {
2469
2470
 
2470
2471
  [ -n "$log_path" ] || log_path="$LOGS_DIR/$branch.log"
2471
2472
 
2472
- # A remote run is relayed, not re-launched: no cap slot, no lane (REMOTE
2473
- # DISPATCH). The pairs stay in the mirror, which the next `sync` replaces.
2474
- if record_is_remote "$branch"; then
2475
- relay_remote_answers "$branch" "$clar_dir" "$log_path" "$answered_set"
2476
- return
2477
- fi
2478
-
2479
2473
  local current
2480
2474
  current="$(running_count)"
2481
2475
  if [ "$current" -ge "$MAX_PARALLEL_RUNS" ]; then
@@ -2505,48 +2499,6 @@ resume_parked_run() {
2505
2499
  return 0
2506
2500
  }
2507
2501
 
2508
- # remote_relay_call <log_path> <remote-run.sh argument>...
2509
- #
2510
- # One relay's `remote-run.sh` call, from the main checkout, its output appended
2511
- # to <log_path>. Returns the script's exit code (0 sent, 2 refused, 3 `gh`
2512
- # failed) and leaves its first output line in REMOTE_RELAY_FIRST for the
2513
- # caller's one log line.
2514
- remote_relay_call() {
2515
- local log_path="$1" out rc
2516
- shift
2517
- out="$(bash "$REMOTE_RUN" "$@" --repo "$MAIN_REPO" 2>&1)"
2518
- rc=$?
2519
- [ -z "$out" ] || printf '%s\n' "$out" >>"$log_path"
2520
- REMOTE_RELAY_FIRST="$(printf '%s\n' "$out" | sed -n '1p')"
2521
- return "$rc"
2522
- }
2523
-
2524
- # relay_remote_answers <branch> <clar_dir> <log_path> <answered_set>
2525
- #
2526
- # resume_parked_run's remote arm: the answered set sent as a chain-0 `--resume
2527
- # answer` dispatch, with `--park-loop-clear` while clear_park_loops has left
2528
- # `park_loop_clear_pending` set. 0 dispatched; 1 refused or `gh` failed, the
2529
- # record left `parked` and every file where it is, so the next pass retries.
2530
- relay_remote_answers() {
2531
- local branch="$1" clar_dir="$2" log_path="$3" answered_set="$4" rc
2532
- local -a plc=()
2533
- [ "$(registry_get "$branch" park_loop_clear_pending)" = "1" ] && plc=(--park-loop-clear)
2534
- remote_relay_call "$log_path" dispatch "$branch" --engine "$(registry_get "$branch" engine)" \
2535
- --resume answer --answers-from "$clar_dir" --indexes "$answered_set" ${plc[@]+"${plc[@]}"} --chain 0
2536
- rc=$?
2537
- if [ "$rc" -ne 0 ]; then
2538
- log "remote-run.sh dispatch --resume answer for '$branch' failed (exit $rc): ${REMOTE_RELAY_FIRST:-no output} — leaving it parked"
2539
- return 1
2540
- fi
2541
- log "relayed the answers ($answered_set) of parked remote run '$branch' to GitHub Actions"
2542
- registry_set "$branch" status running
2543
- registry_set "$branch" resumed_at "$(date '+%Y-%m-%dT%H:%M:%S')"
2544
- registry_set "$branch" resume_kind answer
2545
- registry_set "$branch" park_loop_clear_pending ""
2546
- notify resumed "$branch" "$log_path" "answered clarification(s) #$answered_set, dispatched"
2547
- return 0
2548
- }
2549
-
2550
2502
  # Every `parked` record, offered to the resume above. The kill switch skips the
2551
2503
  # WHOLE pass rather than each record, so an operator's brake costs one log line
2552
2504
  # in tick() instead of one per parked branch; the cap is per-record, because a
@@ -2572,14 +2524,15 @@ EOF
2572
2524
  # run's working copy; the file is removed so the clear is spent once. Answering a
2573
2525
  # question never releases the hold — only this file does. A missing working copy
2574
2526
  # or an unresolvable state directory leaves the record held. A remote record is
2575
- # also marked `park_loop_clear_pending`, because the count that matters lives in
2576
- # the job's bundle: the answer relay carries the clear to it.
2527
+ # skipped: its hold is cleared by the user's own answer or resume command, which
2528
+ # sends `--park-loop-clear` to the job (REMOTE DISPATCH).
2577
2529
  clear_park_loops() {
2578
2530
  registry_init
2579
2531
  local b worktree state_rel clear_file
2580
2532
  while IFS= read -r b; do
2581
2533
  [ -n "$b" ] || continue
2582
2534
  [ "$(registry_get "$b" status)" = "park_loop" ] || continue
2535
+ record_is_remote "$b" && continue
2583
2536
  worktree="$(registry_get "$b" worktree)"
2584
2537
  [ -n "$worktree" ] || continue
2585
2538
  [ -d "$worktree" ] || {
@@ -2595,7 +2548,6 @@ clear_park_loops() {
2595
2548
  rm -f "$clear_file"
2596
2549
  registry_set "$b" park_loop_cycles 0
2597
2550
  registry_set "$b" status parked
2598
- record_is_remote "$b" && registry_set "$b" park_loop_clear_pending 1
2599
2551
  log "park loop cleared for '$b' (PARK_LOOP_CLEAR found) — back to parked"
2600
2552
  done <<EOF
2601
2553
  $(registry_branches)
@@ -2647,12 +2599,12 @@ begin_pause_resume() {
2647
2599
  #
2648
2600
  # Resume one paused run if a RESUME trigger has landed in its working copy.
2649
2601
  # Return codes, the missing-working-copy outcome and the kill-switch/cap ordering
2650
- # are resume_parked_run's, for the same reasons. A remote record is relayed as a
2651
- # chain-0 `--resume pause` dispatch: 0 sent, 1 refused or `gh` failed with
2652
- # nothing removed.
2602
+ # are resume_parked_run's, for the same reasons. A remote record returns 1 and
2603
+ # logs nothing, as there.
2653
2604
  resume_paused_run() {
2654
2605
  local branch="$1"
2655
2606
  local worktree log_path
2607
+ record_is_remote "$branch" && return 1
2656
2608
  worktree="$(registry_get "$branch" worktree)"
2657
2609
  log_path="$(registry_get "$branch" log_path)"
2658
2610
  [ -n "$worktree" ] || return 1
@@ -2683,23 +2635,6 @@ resume_paused_run() {
2683
2635
 
2684
2636
  [ -n "$log_path" ] || log_path="$LOGS_DIR/$branch.log"
2685
2637
 
2686
- # A remote run is relayed, not re-launched: no cap slot, no lane (REMOTE
2687
- # DISPATCH). The sentinels go only once the dispatch was sent.
2688
- if record_is_remote "$branch"; then
2689
- local rc
2690
- remote_relay_call "$log_path" dispatch "$branch" --engine "$(registry_get "$branch" engine)" \
2691
- --resume pause --chain 0
2692
- rc=$?
2693
- if [ "$rc" -ne 0 ]; then
2694
- log "remote-run.sh dispatch --resume pause for '$branch' failed (exit $rc): ${REMOTE_RELAY_FIRST:-no output} — leaving it paused"
2695
- return 1
2696
- fi
2697
- log "relayed the RESUME of paused remote run '$branch' to GitHub Actions"
2698
- begin_pause_resume "$branch" "$state_abs"
2699
- notify resumed "$branch" "$log_path" "after pause, dispatched"
2700
- return 0
2701
- fi
2702
-
2703
2638
  local current
2704
2639
  current="$(running_count)"
2705
2640
  if [ "$current" -ge "$MAX_PARALLEL_RUNS" ]; then
@@ -2742,39 +2677,6 @@ $(registry_branches)
2742
2677
  EOF
2743
2678
  }
2744
2679
 
2745
- # THE PAUSE RELAY (REMOTE DISPATCH). A `running` remote record whose mirror
2746
- # holds `<state_dir>/PAUSE` — a user's pause — is sent `remote-run.sh pause`;
2747
- # on exit 0 the mirror's PAUSE goes and `pause_relayed_at` is stamped, and on
2748
- # any other exit both stay for the next pass. Called from `tick` ahead of the
2749
- # kill switch, because a pause starts nothing; it consults neither the cap nor
2750
- # the lane.
2751
- relay_remote_pauses() {
2752
- registry_init
2753
- local b wt state_rel rc log_path
2754
- while IFS= read -r b; do
2755
- [ -n "$b" ] || continue
2756
- record_is_remote "$b" || continue
2757
- [ "$(registry_get "$b" status)" = "running" ] || continue
2758
- wt="$(registry_get "$b" worktree)"
2759
- [ -n "$wt" ] && [ -d "$wt" ] || continue
2760
- state_rel="$(run_state_dir "$wt")" || continue
2761
- [ -f "$wt/$state_rel/PAUSE" ] || continue
2762
- log_path="$(registry_get "$b" log_path)"
2763
- [ -n "$log_path" ] || log_path="$LOGS_DIR/$b.log"
2764
- remote_relay_call "$log_path" pause "$b"
2765
- rc=$?
2766
- if [ "$rc" -ne 0 ]; then
2767
- log "remote-run.sh pause for '$b' failed (exit $rc): ${REMOTE_RELAY_FIRST:-no output} — PAUSE left in the mirror"
2768
- continue
2769
- fi
2770
- rm -f "$wt/$state_rel/PAUSE"
2771
- registry_set "$b" pause_relayed_at "$(date +%s)"
2772
- log "relayed the PAUSE of remote run '$b' to GitHub Actions"
2773
- done <<EOF
2774
- $(registry_branches)
2775
- EOF
2776
- }
2777
-
2778
2680
  # -----------------------------------------------------------------------------
2779
2681
  # THE STALENESS WATCHDOG. The header states what this pass heals that the
2780
2682
  # reconcile pass cannot see, why a stale mtime ALONE never kills, why the
@@ -3023,29 +2925,27 @@ reject_preserving_status() {
3023
2925
  # remote_commit_and_push <branch> <worktree> <log_path> <file> <fname> <dest> <rel> <subject> <what>
3024
2926
  #
3025
2927
  # The remote arm's commit-and-push, BLOCKING (REMOTE DISPATCH in the header):
3026
- # 0 = <dest> is committed and origin/<branch> carries HEAD; 1 = it is not, and
2928
+ # 0 = <rel> is committed and origin/<branch> carries HEAD; 1 = it is not, and
3027
2929
  # fail_before_launch has already run, so the caller dispatches nothing. The
3028
- # same staging, identical-re-drop skip and wrappers the local arm uses; the
3029
- # push stays a separate statement from the commit.
2930
+ # library's staging, identical-re-drop skip and landed test (THE ARTIFACT
2931
+ # PLACEMENT in lib/harness-run-lib.sh) with the local arm's wrappers; the push
2932
+ # stays a separate statement from the commit. <dest> is the caller's.
3030
2933
  remote_commit_and_push() {
3031
2934
  local branch="$1" worktree="$2" log_path="$3" file="$4" fname="$5"
3032
- local dest="$6" rel="$7" subject="$8" what="$9" head upstream
2935
+ local dest="$6" rel="$7" subject="$8" what="$9" rc=0
3033
2936
 
3034
- git -C "$worktree" add "$dest"
3035
- if git -C "$worktree" diff --cached --quiet "$dest"; then
3036
- log "the $what for '$branch' is already committed (identical re-drop) — skipping the commit, pushing anyway"
3037
- elif "$COMMIT_ON_BRANCH" --repo "$worktree" "$rel" -- "$subject" >>"$log_path" 2>&1; then
3038
- log "committed the $what for '$branch' ($subject)"
3039
- else
3040
- log "commit-on-branch.sh could not commit the $what for '$branch' — not dispatching (see $log_path)"
3041
- fail_before_launch "$branch" "$file" "$fname" "(commit-on-branch.sh could not commit the $what — nothing dispatched; see $log_path)"
3042
- return 1
3043
- fi
2937
+ hr_commit_placed "$COMMIT_ON_BRANCH" "$worktree" "$rel" "$subject" >>"$log_path" 2>&1 || rc=$?
2938
+ case "$rc" in
2939
+ 3) log "the $what for '$branch' is already committed (identical re-drop) — skipping the commit, pushing anyway" ;;
2940
+ 0) log "committed the $what for '$branch' ($subject)" ;;
2941
+ *)
2942
+ log "commit-on-branch.sh could not commit the $what for '$branch' — not dispatching (see $log_path)"
2943
+ fail_before_launch "$branch" "$file" "$fname" "(commit-on-branch.sh could not commit the $what — nothing dispatched; see $log_path)"
2944
+ return 1
2945
+ ;;
2946
+ esac
3044
2947
 
3045
- "$PUSH_BRANCH" "$worktree" >>"$log_path" 2>&1
3046
- head="$(git -C "$worktree" rev-parse --verify --quiet HEAD)" || head=""
3047
- upstream="$(git -C "$worktree" rev-parse --verify --quiet "refs/remotes/origin/$branch")" || upstream=""
3048
- if [ -z "$head" ] || [ "$head" != "$upstream" ]; then
2948
+ if ! hr_push_landed "$PUSH_BRANCH" "$worktree" "$branch" >>"$log_path" 2>&1; then
3049
2949
  log "push-branch.sh did not bring origin/$branch to HEAD after the $what for '$branch' — not dispatching (see $log_path)"
3050
2950
  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)"
3051
2951
  return 1
@@ -3067,18 +2967,8 @@ remote_commit_and_push() {
3067
2967
  # ^(.+)_review(_[0-9]+)?\.md$ -> user_review engine, reuse-else-recreate
3068
2968
  # ^(.+)_docs\.md$ -> docs engine, a fresh working copy
3069
2969
  #
3070
- # The task-prompt pattern is tested FIRST (the more specific suffix), but the
3071
- # anchored SUFFIX regexes are mutually exclusive by construction: a filename
3072
- # cannot end in more than one of `_task_prompt.md` / `_review[_<n>].md` /
3073
- # `_docs.md`, so a branch whose own name contains `review` or `task_prompt`
3074
- # cannot be mis-routed — `foo_review_task_prompt.md` is the task engine on branch
3075
- # `foo_review`, and `foo_task_prompt_review.md` is the review engine on branch
3076
- # `foo_task_prompt`. POSIX leftmost-longest matching of the greedy `(.+)` derives
3077
- # the right branch from a round-suffixed name: `foo_review_2.md` -> branch `foo`
3078
- # (the `_2` is consumed by the optional `(_[0-9]+)?`), while
3079
- # `foo_review_2_review.md` -> branch `foo_review_2`. THE WATCHER DERIVES ONLY THE
3080
- # BRANCH, never the round: the engine resolves the latest round itself, inside
3081
- # the working copy, which is why nothing here has to remember one.
2970
+ # The library owns these patterns and their order: `hr_inbox_route_var` in
2971
+ # lib/harness-run-lib.sh, whose comment states why they cannot mis-route.
3082
2972
  #
3083
2973
  # A filename matching none of the three is logged and ARCHIVED rather than left
3084
2974
  # where it is, so it is not re-logged on every pass for as long as the watcher
@@ -3091,24 +2981,13 @@ process_inbox_file() {
3091
2981
 
3092
2982
  # (1) Route the filename to its pairing and derive <branch> — see above.
3093
2983
  local branch engine_kind
3094
- branch="$(printf '%s' "$fname" | sed -nE 's/^(.+)_task_prompt\.md$/\1/p')"
3095
- if [ -n "$branch" ]; then
3096
- engine_kind="task"
3097
- else
3098
- branch="$(printf '%s' "$fname" | sed -nE 's/^(.+)_review(_[0-9]+)?\.md$/\1/p')"
3099
- if [ -n "$branch" ]; then
3100
- engine_kind="user_review"
3101
- else
3102
- branch="$(printf '%s' "$fname" | sed -nE 's/^(.+)_docs\.md$/\1/p')"
3103
- if [ -n "$branch" ]; then
3104
- engine_kind="docs"
3105
- else
3106
- log "rejecting '$fname': not a <branch>_task_prompt.md / <branch>_review[_<n>].md / <branch>_docs.md file — skipping"
3107
- mv "$file" "$ARCHIVE_DIR/rejected_$(date '+%Y%m%d%H%M%S')_$fname" 2>/dev/null || rm -f "$file"
3108
- return 0
3109
- fi
3110
- fi
2984
+ if ! hr_inbox_route_var "$fname"; then
2985
+ log "rejecting '$fname': not a <branch>_task_prompt.md / <branch>_review[_<n>].md / <branch>_docs.md file — skipping"
2986
+ mv "$file" "$ARCHIVE_DIR/rejected_$(date '+%Y%m%d%H%M%S')_$fname" 2>/dev/null || rm -f "$file"
2987
+ return 0
3111
2988
  fi
2989
+ branch="$HR_INBOX_BRANCH"
2990
+ engine_kind="$HR_INBOX_KIND"
3112
2991
 
3113
2992
  # The central log every step below appends to. A branch derived from a FILENAME
3114
2993
  # cannot contain a `/`, so this name needs no sanitizing — unlike the
@@ -3278,10 +3157,11 @@ process_inbox_file() {
3278
3157
 
3279
3158
  # (3a) Copy the dropped prompt into the working copy, then archive the inbox
3280
3159
  # file so it is not processed again.
3281
- local prompt_rel="$state_rel/task_prompts/${branch}_task_prompt.md"
3282
- local prompt_dest="$worktree/$prompt_rel"
3283
- mkdir -p "$worktree/$state_rel/task_prompts"
3284
- cp "$file" "$prompt_dest"
3160
+ local prompt_rel prompt_dest prompt_subject prompt_rc=0
3161
+ prompt_rel="$(hr_task_prompt_rel "$state_rel" "$branch")"
3162
+ prompt_dest="$worktree/$prompt_rel"
3163
+ prompt_subject="$(hr_task_prompt_subject "$branch")"
3164
+ hr_place_artifact "$worktree" "$file" "$prompt_rel"
3285
3165
  mv "$file" "$ARCHIVE_DIR/$(date '+%Y%m%d%H%M%S')_$fname" 2>/dev/null || rm -f "$file"
3286
3166
  log "copied the prompt -> $prompt_dest; archived the inbox file"
3287
3167
 
@@ -3296,13 +3176,14 @@ process_inbox_file() {
3296
3176
  #
3297
3177
  # Only the prompt is staged, by explicit path — never `git add -A` or
3298
3178
  # `git add .`, matching the no-blanket-add rule every unattended commit point
3299
- # in this family follows. The `diff --cached --quiet` pre-check is what makes
3179
+ # in this family follows. The nothing-staged pre-check is what makes
3300
3180
  # an identical re-drop of an already-committed prompt a no-op instead of an
3301
3181
  # empty commit; when there IS a diff, the WRAPPER does the real staging and
3302
3182
  # the commit, so this commit point inherits its protected-branch refusal
3303
3183
  # rather than re-implementing it. The wrapper stages paths RELATIVE TO THE
3304
- # REPOSITORY TOP, so it is handed the repo-relative path; the absolute one is
3305
- # `git -C "$worktree"`-scoped and only feeds the skip pre-check.
3184
+ # REPOSITORY TOP, so it is handed the repo-relative path. The placement
3185
+ # itself lives in lib/harness-run-lib.sh (THE ARTIFACT PLACEMENT), shared
3186
+ # with a job that starts a run.
3306
3187
  #
3307
3188
  # A FAILURE AT EITHER STEP IS LOGGED AND THE RUN LAUNCHES ANYWAY: a prompt
3308
3189
  # commit that did not land has to be VISIBLE, and it must never be the reason
@@ -3313,20 +3194,18 @@ process_inbox_file() {
3313
3194
  # dispatch instead (REMOTE DISPATCH in the header).
3314
3195
  if [ "$remote" = 1 ]; then
3315
3196
  remote_commit_and_push "$branch" "$worktree" "$log_path" "$file" "$fname" \
3316
- "$prompt_dest" "$prompt_rel" "chore: add task prompt for $branch" "task prompt" || return 0
3197
+ "$prompt_dest" "$prompt_rel" "$prompt_subject" "task prompt" || return 0
3317
3198
  else
3318
- git -C "$worktree" add "$prompt_dest"
3319
- if git -C "$worktree" diff --cached --quiet "$prompt_dest"; then
3320
- log "the task prompt for '$branch' is already committed (identical re-drop) — skipping the commit"
3321
- elif "$COMMIT_ON_BRANCH" --repo "$worktree" \
3322
- "$prompt_rel" \
3323
- -- "chore: add task prompt for $branch" >>"$log_path" 2>&1; then
3324
- log "committed the task prompt for '$branch' (chore: add task prompt for $branch)"
3325
- "$PUSH_BRANCH" "$worktree" >>"$log_path" 2>&1 ||
3326
- log "WARNING: push-branch.sh failed after the task-prompt commit for '$branch' — continuing"
3327
- else
3328
- log "WARNING: could not commit the task prompt for '$branch' — launching anyway (its working-tree-clean precondition may be dishonest; see $log_path)"
3329
- fi
3199
+ hr_commit_placed "$COMMIT_ON_BRANCH" "$worktree" "$prompt_rel" "$prompt_subject" >>"$log_path" 2>&1 || prompt_rc=$?
3200
+ case "$prompt_rc" in
3201
+ 3) log "the task prompt for '$branch' is already committed (identical re-drop) — skipping the commit" ;;
3202
+ 0)
3203
+ log "committed the task prompt for '$branch' ($prompt_subject)"
3204
+ "$PUSH_BRANCH" "$worktree" >>"$log_path" 2>&1 ||
3205
+ log "WARNING: push-branch.sh failed after the task-prompt commit for '$branch' — continuing"
3206
+ ;;
3207
+ *) log "WARNING: could not commit the task prompt for '$branch' — launching anyway (its working-tree-clean precondition may be dishonest; see $log_path)" ;;
3208
+ esac
3330
3209
  fi
3331
3210
  elif [ "$engine_kind" = "docs" ]; then
3332
3211
  # (2c) Docs path: the task path's strategy exactly — a FRESH working copy off
@@ -3354,26 +3233,24 @@ process_inbox_file() {
3354
3233
  # rationale is stated once, above.
3355
3234
  local docs_rel="$state_rel/docs_catalog/${branch}_docs.md"
3356
3235
  local docs_dest="$worktree/$docs_rel"
3357
- mkdir -p "$worktree/$state_rel/docs_catalog"
3358
- cp "$file" "$docs_dest"
3236
+ local docs_subject="chore: add docs checklist for $branch" docs_rc=0
3237
+ hr_place_artifact "$worktree" "$file" "$docs_rel"
3359
3238
  mv "$file" "$ARCHIVE_DIR/$(date '+%Y%m%d%H%M%S')_$fname" 2>/dev/null || rm -f "$file"
3360
3239
  log "copied the docs checklist -> $docs_dest; archived the inbox file"
3361
3240
  if [ "$remote" = 1 ]; then
3362
3241
  remote_commit_and_push "$branch" "$worktree" "$log_path" "$file" "$fname" \
3363
- "$docs_dest" "$docs_rel" "chore: add docs checklist for $branch" "docs checklist" || return 0
3242
+ "$docs_dest" "$docs_rel" "$docs_subject" "docs checklist" || return 0
3364
3243
  else
3365
- git -C "$worktree" add "$docs_dest"
3366
- if git -C "$worktree" diff --cached --quiet "$docs_dest"; then
3367
- log "the docs checklist for '$branch' is already committed (identical re-drop) — skipping the commit"
3368
- elif "$COMMIT_ON_BRANCH" --repo "$worktree" \
3369
- "$docs_rel" \
3370
- -- "chore: add docs checklist for $branch" >>"$log_path" 2>&1; then
3371
- log "committed the docs checklist for '$branch' (chore: add docs checklist for $branch)"
3372
- "$PUSH_BRANCH" "$worktree" >>"$log_path" 2>&1 ||
3373
- log "WARNING: push-branch.sh failed after the docs-checklist commit for '$branch' — continuing"
3374
- else
3375
- log "WARNING: could not commit the docs checklist for '$branch' — launching anyway (see $log_path)"
3376
- fi
3244
+ hr_commit_placed "$COMMIT_ON_BRANCH" "$worktree" "$docs_rel" "$docs_subject" >>"$log_path" 2>&1 || docs_rc=$?
3245
+ case "$docs_rc" in
3246
+ 3) log "the docs checklist for '$branch' is already committed (identical re-drop) — skipping the commit" ;;
3247
+ 0)
3248
+ log "committed the docs checklist for '$branch' ($docs_subject)"
3249
+ "$PUSH_BRANCH" "$worktree" >>"$log_path" 2>&1 ||
3250
+ log "WARNING: push-branch.sh failed after the docs-checklist commit for '$branch' — continuing"
3251
+ ;;
3252
+ *) log "WARNING: could not commit the docs checklist for '$branch' — launching anyway (see $log_path)" ;;
3253
+ esac
3377
3254
  fi
3378
3255
  else
3379
3256
  # (2b) Review path: REUSE the branch's existing working copy when it is
@@ -3466,7 +3343,7 @@ process_inbox_file() {
3466
3343
  log "copied the review -> $review_dest; archived the inbox file"
3467
3344
  if [ "$remote" = 1 ]; then
3468
3345
  remote_commit_and_push "$branch" "$worktree" "$log_path" "$file" "$fname" \
3469
- "$review_dest" "$review_rel" "chore: add user review for $branch" "user review" || return 0
3346
+ "$review_dest" "$review_rel" "$(hr_user_review_subject "$branch")" "user review" || return 0
3470
3347
  fi
3471
3348
  fi
3472
3349
 
@@ -3735,8 +3612,7 @@ usage_gate() {
3735
3612
  local b status pb ra wt lp state_rel="" state_abs=""
3736
3613
  while IFS= read -r b; do
3737
3614
  [ -n "$b" ] || continue
3738
- # The auto-resume side skips a remote run: its job gates itself, and
3739
- # resume_paused_run relays only a user's RESUME.
3615
+ # The auto-resume side skips a remote run: its job gates itself.
3740
3616
  record_is_remote "$b" && continue
3741
3617
  status="$(registry_get "$b" status)"
3742
3618
  pb="$(registry_get "$b" paused_by)"
@@ -3921,11 +3797,8 @@ EOF
3921
3797
  }
3922
3798
 
3923
3799
  # -----------------------------------------------------------------------------
3924
- # One pass. The relay of a remote run's pause first, then the kill switch, so an
3925
- # operator's brake beats everything else — relaying a remote run's pause is the
3926
- # one action that still runs under the brake, because it starts nothing (see
3927
- # REMOTE DISPATCH) — then the reconcile that frees capacity for the passes that
3928
- # read the cap.
3800
+ # One pass. The kill switch first, so an operator's brake beats everything
3801
+ # else, then the reconcile that frees capacity for the passes that read the cap.
3929
3802
  # -----------------------------------------------------------------------------
3930
3803
  tick() {
3931
3804
  # Drop the library's per-process cache so an edit to `harness.config.json` is
@@ -3935,8 +3808,6 @@ tick() {
3935
3808
  hr_config_reset
3936
3809
  hr_config_load "$MAIN_REPO" || :
3937
3810
 
3938
- relay_remote_pauses
3939
-
3940
3811
  if kill_switch_active; then
3941
3812
  log "global kill switch active — not launching or resuming runs this pass"
3942
3813
  # A braked watcher must not sit on the machine-level lane: it is starting
@@ -4342,14 +4213,14 @@ run_job() {
4342
4213
  # run over rather than waiting on nothing.
4343
4214
  decision=wait-poller
4344
4215
  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"
4345
- 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"
4216
+ 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)")"
4346
4217
  break
4347
4218
  fi
4348
4219
  ;;
4349
4220
  *)
4350
4221
  job_auto_resume "$branch" "$worktree" "$log_path" "$state_abs" "an overload self-pause" && continue
4351
4222
  detail="paused itself on API overload; automatic resumes exhausted"
4352
- notify paused "$branch" "$log_path" "paused itself on API overload — run /autonomous-sdlc-harness:branch-resume $branch to continue"
4223
+ 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)")"
4353
4224
  break
4354
4225
  ;;
4355
4226
  esac
@@ -4377,6 +4248,7 @@ run_job() {
4377
4248
  # A reason recorded for a pause the run finished before honouring.
4378
4249
  [ "$final" = "paused" ] || registry_set "$branch" pause_reason ""
4379
4250
  [ -n "$detail" ] || detail="the run ended $final in this job"
4251
+ [ "$final" != "failed" ] || job_report failed "$branch" "$log_path"
4380
4252
  job_write_status "$branch" "$remote_status" "$decision" "$detail"
4381
4253
  echo "job: $final $decision"
4382
4254
  exit 0