@@ -52,12 +52,23 @@ DRIVER_PR=${DRIVER_PR:-}
5252DRIVER_TICK_SECONDS=${DRIVER_TICK_SECONDS:- 30}
5353DRIVER_ONESHOT=${DRIVER_ONESHOT:- 0}
5454
55+ # Snapshot whether the lane was bound externally via env at startup. An
56+ # env-bound lane drives one PR to terminal and exits (legacy bring-up
57+ # shape); a claim-bound lane (no DRIVER_PR at startup) claims jobs from
58+ # journal/jobs/open/ and resumes the loop after each terminal so systemd
59+ # keeps the unit alive across many jobs.
60+ DRIVER_PR_FROM_ENV=$DRIVER_PR
61+ DRIVER_WORKFLOW_FROM_ENV=$DRIVER_WORKFLOW
62+
5563test -d " $GARDEN_ROOT " || { echo " driver: GARDEN_ROOT not a directory: $GARDEN_ROOT " >&2 ; exit 1; }
5664test -d " $GARDEN_JOURNAL " || { echo " driver: GARDEN_JOURNAL not a directory: $GARDEN_JOURNAL " >&2 ; exit 1; }
5765
5866STATE_DIR=" $GARDEN_JOURNAL /drivers/$GARDEN_HOST "
5967STATE_FILE=" $STATE_DIR /$LANE .md"
6068SUBSCRIPTIONS_FILE=" $STATE_DIR /$LANE .subscriptions"
69+ # Tracks which jobs/claimed/...md path this lane is currently working,
70+ # so a daemon restart can resume the same claim instead of stranding it.
71+ CLAIMED_JOB_FILE=" $STATE_DIR /$LANE .claimed-job"
6172mkdir -p " $STATE_DIR "
6273
6374# --- transcript and trap setup -----------------------------------------
340351 ${DRIVER_PR: +--pr " ${DRIVER_PR##*# } " } \
341352 --eligible " $role " \
342353 < /tmp/driver-job-body.$$
354+ local rc=$?
343355 rm -f /tmp/driver-job-body.$$
356+ return " $rc "
344357}
345358
346359escalate_to_claude () {
@@ -492,10 +505,116 @@ _self_improve_invoke_async() {
492505 } >> " $improvements_file " 2> /dev/null || true
493506}
494507
508+ # --- job-board claim handling -------------------------------------------
509+
510+ read_claimed_job () {
511+ [ -f " $CLAIMED_JOB_FILE " ] || return 0
512+ cat " $CLAIMED_JOB_FILE "
513+ }
514+
515+ write_claimed_job () {
516+ printf ' %s\n' " $1 " > " $CLAIMED_JOB_FILE "
517+ }
518+
519+ clear_claimed_job () {
520+ rm -f " $CLAIMED_JOB_FILE "
521+ }
522+
523+ # Parse a claimed job's frontmatter and populate DRIVER_WORKFLOW and
524+ # DRIVER_PR. The post-job.sh schema emits `verb:` at the top level and
525+ # `target.repo` / `target.pr` indented under `target:`.
526+ parse_claimed_job_into_env () {
527+ local path=$1
528+ local verb repo pr
529+ verb=$( sed -n ' s/^verb: //p' " $path " | head -1)
530+ repo=$( sed -n ' s/^ repo: //p' " $path " | head -1)
531+ pr=$( sed -n ' s/^ pr: //p' " $path " | head -1)
532+ DRIVER_WORKFLOW=$verb
533+ if [ -n " $repo " ] && [ " $repo " != " null" ] && [ -n " $pr " ] && [ " $pr " != " null" ]; then
534+ DRIVER_PR=" ${repo} #${pr} "
535+ else
536+ DRIVER_PR=" "
537+ fi
538+ }
539+
540+ # Race to claim one open job. Echoes the claimed path on success;
541+ # returns 1 if the inbox is empty or every visible job was lost to
542+ # another claimant.
543+ try_claim_one () {
544+ local open_dir=" $GARDEN_JOURNAL /jobs/open"
545+ [ -d " $open_dir " ] || return 1
546+ local f rel claimed
547+ for f in " $open_dir " /* .md; do
548+ [ -f " $f " ] || continue
549+ rel=" jobs/open/$( basename " $f " ) "
550+ claimed=$( GARDEN_ROLE=driver " $GARDEN_ROOT /skills/job-board/claim-job.sh" " $rel " 2> /dev/null) || continue
551+ [ " $claimed " = " lost-race" ] && continue
552+ [ -n " $claimed " ] || continue
553+ printf ' %s\n' " $claimed "
554+ return 0
555+ done
556+ return 1
557+ }
558+
559+ # Returns 0 when this lane has a claimed job (existing or newly claimed)
560+ # and DRIVER_PR/DRIVER_WORKFLOW are populated from its frontmatter.
561+ # Returns 1 when no job is available — the caller should idle this tick.
562+ ensure_claimed_job () {
563+ local recorded
564+ recorded=$( read_claimed_job)
565+ if [ -n " $recorded " ] && [ -f " $GARDEN_JOURNAL /$recorded " ]; then
566+ parse_claimed_job_into_env " $GARDEN_JOURNAL /$recorded "
567+ return 0
568+ fi
569+ # Stale or absent — try to claim a new one.
570+ local newly
571+ newly=$( try_claim_one) || return 1
572+ write_claimed_job " $newly "
573+ parse_claimed_job_into_env " $GARDEN_JOURNAL /$newly "
574+ # Reset state file for the new job. The workflow runs from `initial`.
575+ write_state " initial" " null" " claimed $newly "
576+ return 0
577+ }
578+
579+ # Move the current claimed job to done/ via complete-job.sh, then clear
580+ # the per-lane claim and workflow bindings so the next tick claims again.
581+ release_claimed_job_done () {
582+ local recorded
583+ recorded=$( read_claimed_job)
584+ if [ -n " $recorded " ] && [ -x " $GARDEN_ROOT /skills/job-board/complete-job.sh" ]; then
585+ " $GARDEN_ROOT /skills/job-board/complete-job.sh" " $recorded " done > /dev/null 2>&1 || true
586+ fi
587+ clear_claimed_job
588+ DRIVER_PR=" "
589+ DRIVER_WORKFLOW=" "
590+ }
591+
495592# --- main loop ----------------------------------------------------------
496593
497594run_once () {
498595 local state next_directive
596+
597+ # Claim-bound lanes (no DRIVER_PR at startup) acquire their PR and
598+ # workflow from the job board on each tick. Env-bound lanes skip the
599+ # board entirely and drive the single PR they were launched with.
600+ if [ -z " $DRIVER_PR_FROM_ENV " ]; then
601+ if ! ensure_claimed_job; then
602+ # Inbox is empty. Surface the wait in the state file so external
603+ # observers see the lane is idle, then return so the outer loop
604+ # sleeps until the next tick.
605+ DRIVER_WORKFLOW=idle
606+ write_state " idle" " null" " no open jobs; polling inbox"
607+ return 0
608+ fi
609+ # Refresh the per-lane subscription advertisement to match the
610+ # currently-claimed PR.
611+ if [ -n " $DRIVER_PR " ]; then
612+ printf ' %s\n' " $DRIVER_PR " > " $SUBSCRIPTIONS_FILE "
613+ else
614+ : > " $SUBSCRIPTIONS_FILE "
615+ fi
616+ fi
617+
499618 state=$( read_state)
500619
501620 case " $DRIVER_WORKFLOW " in
@@ -553,8 +672,14 @@ run_once() {
553672 terminal:* )
554673 local final=${next_directive# terminal: }
555674 write_state " $final " " null" " driver lane $LANE reached terminal state $final "
556- DRIVER_EXPECTED_EXIT=1
557- return 2 # signal terminal to outer loop
675+ if [ -n " $DRIVER_PR_FROM_ENV " ]; then
676+ # Env-bound: drove the one PR we were launched with; exit cleanly.
677+ DRIVER_EXPECTED_EXIT=1
678+ return 2
679+ fi
680+ # Claim-bound: release this job and keep ticking so the next tick
681+ # claims the next one. The outer loop stays alive.
682+ release_claimed_job_done
558683 ;;
559684 * )
560685 echo " driver: workflow returned malformed directive: $next_directive " >&2
@@ -565,18 +690,31 @@ run_once() {
565690}
566691
567692main () {
568- select_workflow
693+ # Env-bound lanes resolve their workflow once at startup. Claim-bound
694+ # lanes leave DRIVER_WORKFLOW unset and let ensure_claimed_job pick it
695+ # up per-job from each claimed frontmatter.
696+ if [ -n " $DRIVER_PR_FROM_ENV " ]; then
697+ select_workflow
698+ fi
569699
570- # Initialize subscription advertisement.
700+ # Initialize subscription advertisement. Claim-bound lanes refresh
701+ # this each tick after ensure_claimed_job populates DRIVER_PR.
571702 if [ -n " $DRIVER_PR " ]; then
572703 echo " $DRIVER_PR " > " $SUBSCRIPTIONS_FILE "
573704 else
574705 : > " $SUBSCRIPTIONS_FILE "
575706 fi
576707
577- # Initialize state file if missing.
708+ # Initialize state file if missing. Claim-bound lanes start "idle"
709+ # until a job is claimed; env-bound lanes start at the workflow's
710+ # initial state.
578711 if [ ! -f " $STATE_FILE " ]; then
579- write_state " initial" " null" " driver lane $LANE bootstrap"
712+ if [ -n " $DRIVER_PR_FROM_ENV " ]; then
713+ write_state " initial" " null" " driver lane $LANE bootstrap"
714+ else
715+ DRIVER_WORKFLOW=idle
716+ write_state " idle" " null" " driver lane $LANE bootstrap; awaiting claim"
717+ fi
580718 fi
581719
582720 # Outer loop. The -x transcript capture happens inside the loop body so
0 commit comments