Skip to content

Commit dfd2d29

Browse files
endolinbotclaude
andcommitted
driver: claim work from the journal job board
Each lane now polls journal/jobs/open/ on every tick, races to claim via skills/job-board/claim-job.sh, and parses the claimed job's verb: and target: frontmatter to populate DRIVER_WORKFLOW and DRIVER_PR for the workflow dispatcher. The per-lane tracker at journal/drivers/<host>/<lane>.claimed-job records the active job's path so a daemon restart resumes the same claim instead of orphaning it. On terminal state a claim-bound lane releases the job via complete-job.sh done, clears its bindings, and loops back to claim the next; env-bound lanes (DRIVER_PR set at startup) keep the legacy exit-on-terminal shape. post_job in driver.sh now captures and returns the post-job.sh exit code instead of the `rm -f` cleanup's, so a failed post no longer silently advances the state machine. /journal.git/ is gitignored so a host-local bare repo can serve as the journal's origin remote — necessary so claim-job.sh and complete-job.sh's push race actually completes when the lane's identity has no credentials for kriskowal/garden:journal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2c5671d commit dfd2d29

2 files changed

Lines changed: 145 additions & 6 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Garden working trees managed outside main.
22
# These are separate git worktrees / bare clones; do not track them from main.
33
/journal/
4+
/journal.git/
45
/worktrees/
56
/dispatches/
67

scripts/driver/driver.sh

Lines changed: 144 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,23 @@ DRIVER_PR=${DRIVER_PR:-}
5252
DRIVER_TICK_SECONDS=${DRIVER_TICK_SECONDS:-30}
5353
DRIVER_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+
5563
test -d "$GARDEN_ROOT" || { echo "driver: GARDEN_ROOT not a directory: $GARDEN_ROOT" >&2; exit 1; }
5664
test -d "$GARDEN_JOURNAL" || { echo "driver: GARDEN_JOURNAL not a directory: $GARDEN_JOURNAL" >&2; exit 1; }
5765

5866
STATE_DIR="$GARDEN_JOURNAL/drivers/$GARDEN_HOST"
5967
STATE_FILE="$STATE_DIR/$LANE.md"
6068
SUBSCRIPTIONS_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"
6172
mkdir -p "$STATE_DIR"
6273

6374
# --- transcript and trap setup -----------------------------------------
@@ -340,7 +351,9 @@ EOF
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

346359
escalate_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

497594
run_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

567692
main() {
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

Comments
 (0)