Skip to content

Commit 0ba1aed

Browse files
author
endolinbot
committed
feat(driver): per-tick capture + agent self-improvement loop, CI job (#3)
Per kriskowal CHANGES_REQUESTED review on PR #3: 1. CI workflow at .github/workflows/driver-tests.yml runs `bash tests/driver/run.sh` and `bash skills/cleaner/test-cleaner.sh` on push to main and design/driver, and on PR to main. Uses the ubuntu-latest runner; configures a git identity since the driver tests build a mock journal that commits. 2. roles/driver/driver.sh wraps each loop iteration's stdout+stderr in a per-tick capture file, hashes it via `git hash-object -w --stdin` into the journal object database, and invokes an agent (the PATH-stubbed `claude` shim in tests, or real `claude` in production) with a four-slot prompt naming the capture SHA. The agent's analysis is appended to journal/drivers/<host>/<lane>.improvements.md so a gardener can review it later. The capture-and-analyze step runs after the state transition (so the transcript reflects what the tick actually did) and the agent invocation forks to a background subshell so it does not block the next tick. SELF_IMPROVE_SYNC=1 makes the test harness join synchronously; SELF_IMPROVE_CLAUDE_STUB lets tests inject a deterministic stub agent. 3. New test tests/driver/test_loop_capture_and_self_improve.sh covers: - the per-tick blob lands in the journal object DB (verified via `git cat-file -e <sha>` and a content assertion that the blob carries the `+ run_once` -x trace); - the agent is invoked (the stub records the SHA; in the stubless run the PATH-stubbed claude's log records `claude invoked with -p`); - the per-lane improvements file accumulates a new section per tick (file size grows; section count grows). .gitignore adds explicit negation rules so `.github/` is tracked despite the root-level dotfile-detritus blanket. The blanket exists to keep bot SSH/oauth tokens out of the repo; CI workflows are the one tracked exception today. Existing tests (test_skeleton, test_design_only_happy_path, test_trap_fires_on_error) continue passing; the cleaner skeleton self-test continues passing. Refs: garden#3
1 parent 48afa74 commit 0ba1aed

4 files changed

Lines changed: 378 additions & 3 deletions

File tree

.github/workflows/driver-tests.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: driver-tests
2+
3+
on:
4+
push:
5+
branches:
6+
- design/driver
7+
- main
8+
pull_request:
9+
branches:
10+
- main
11+
12+
jobs:
13+
driver-tests:
14+
name: driver-tests
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Configure local git identity
21+
# The driver tests build a mock journal that runs `git init` and
22+
# commits. GitHub Actions runners do not have a default identity.
23+
run: |
24+
git config --global user.name "ci-bot"
25+
git config --global user.email "ci-bot@example.invalid"
26+
27+
- name: Run driver script tests
28+
run: bash tests/driver/run.sh
29+
30+
- name: Run cleaner skeleton self-test
31+
run: bash skills/cleaner/test-cleaner.sh

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@
1010
# token under .ssh/ and .config/gh/ — lands here. Never committed.
1111
# Pattern matches every top-level dotfile/dir except ./.gitignore itself.
1212
/.[!.]*
13+
14+
# Carve back the few top-level dotfile paths we do want tracked. Add
15+
# more here when a new tracked dotfile path is introduced. The
16+
# .github/ directory holds CI workflows that GitHub reads from the
17+
# repo root.
18+
!/.gitignore
19+
!/.github/
20+
!/.github/**

roles/driver/driver.sh

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,130 @@ escalate_to_claude() {
367367
return 1
368368
}
369369

370+
# --- per-tick capture + self-improvement -------------------------------
371+
372+
# capture_and_self_improve <tick-capture-path> <tick-rc>
373+
#
374+
# Hash the per-tick capture into the journal's object database so the
375+
# transcript survives as a blob, then invoke an agent (claude) with the
376+
# capture SHA and append the agent's analysis to a per-lane
377+
# improvements file at $GARDEN_JOURNAL/drivers/<host>/<lane>.improvements.md.
378+
#
379+
# The agent invocation runs in the background so the next tick is not
380+
# blocked. The agent reads the transcript on demand via
381+
# `git -C <journal> cat-file blob <sha>`; the prompt stays small.
382+
#
383+
# Test harness can stub the agent invocation via SELF_IMPROVE_CLAUDE_STUB
384+
# (path to a script that takes the SHA as its sole positional argument and
385+
# emits the analysis on stdout); when unset, we exec `claude -p <prompt>`
386+
# from PATH (the mock harness PATH-stubs `claude`).
387+
#
388+
# Failures here are silent: self-improvement is best-effort and must not
389+
# crash the driver. Each branch ORs with `:` so a bad git, missing claude,
390+
# or unwritable improvements file leaves the loop unaffected.
391+
capture_and_self_improve() {
392+
local tick_capture=$1
393+
local tick_rc=$2
394+
395+
# Bail if the capture is empty or the journal is not a git repo.
396+
if [ ! -s "$tick_capture" ]; then
397+
return 0
398+
fi
399+
if [ ! -d "$GARDEN_JOURNAL/.git" ] && [ ! -f "$GARDEN_JOURNAL/.git" ]; then
400+
return 0
401+
fi
402+
403+
# Hash the capture into the journal's object DB. The blob is
404+
# unreferenced; git gc will collect it after the grace window unless
405+
# an agent or operator anchors it with refs/captures/...
406+
local capture_sha
407+
capture_sha=$(git -C "$GARDEN_JOURNAL" hash-object -w --stdin < "$tick_capture" 2>/dev/null) || return 0
408+
if [ -z "$capture_sha" ]; then
409+
return 0
410+
fi
411+
412+
local improvements_dir="$GARDEN_JOURNAL/drivers/$GARDEN_HOST"
413+
local improvements_file="$improvements_dir/$LANE.improvements.md"
414+
mkdir -p "$improvements_dir"
415+
416+
# Initialize the improvements file on first run with frontmatter so it
417+
# is grep-able by host/lane and the gardener can pick it up.
418+
if [ ! -f "$improvements_file" ]; then
419+
cat > "$improvements_file" <<EOF
420+
---
421+
host: $GARDEN_HOST
422+
lane: $LANE
423+
kind: driver-self-improvement-log
424+
---
425+
426+
# driver lane $LANE self-improvement log
427+
428+
Per-tick agent analysis of driver loop iterations. Each section names
429+
the tick's transcript SHA (inspect via \`git -C journal cat-file blob <sha>\`)
430+
and the agent's suggestions for self-improvement.
431+
432+
EOF
433+
fi
434+
435+
local iso state
436+
iso=$(date -u +%Y-%m-%dT%H:%M:%SZ)
437+
state=$(read_state 2>/dev/null) || state=unknown
438+
439+
# Build the agent prompt. The four-slot brief mirrors the prompt-on-
440+
# failure-capture pattern: PR id, workflow, state, capture SHA.
441+
local prompt
442+
prompt="Driver loop iteration transcript SHA: $capture_sha
443+
PR: ${DRIVER_PR:-(none)}
444+
Workflow: ${DRIVER_WORKFLOW:-unknown}
445+
State: $state
446+
Tick exit code: $tick_rc
447+
448+
Please analyze the transcript and suggest any self-improvements for
449+
the driver's behavior in this state. Read the transcript on demand via:
450+
git -C $GARDEN_JOURNAL cat-file blob $capture_sha
451+
"
452+
453+
# Invoke the agent. Run in the background so the next tick is not
454+
# blocked. The agent's response is appended atomically once it
455+
# completes; concurrent appends across overlapping ticks are vanishingly
456+
# rare in practice but a flock would close that race if it mattered.
457+
_self_improve_invoke_async "$capture_sha" "$state" "$tick_rc" "$iso" "$prompt" "$improvements_file" &
458+
# We deliberately do not `wait`; the background analyzer survives the
459+
# tick's return and writes its section when it finishes. In oneshot
460+
# mode the test harness joins via the SELF_IMPROVE_SYNC flag below.
461+
if [ "${SELF_IMPROVE_SYNC:-0}" = 1 ]; then
462+
wait
463+
fi
464+
return 0
465+
}
466+
467+
_self_improve_invoke_async() {
468+
local capture_sha=$1
469+
local state=$2
470+
local tick_rc=$3
471+
local iso=$4
472+
local prompt=$5
473+
local improvements_file=$6
474+
475+
local response
476+
if [ -n "${SELF_IMPROVE_CLAUDE_STUB:-}" ]; then
477+
response=$("$SELF_IMPROVE_CLAUDE_STUB" "$capture_sha" 2>/dev/null) || response="(stub returned non-zero)"
478+
elif command -v claude >/dev/null 2>&1; then
479+
response=$(printf '%s' "$prompt" | claude -p 2>/dev/null) || response="(claude invocation failed)"
480+
else
481+
response="(no agent available: claude not on PATH and no stub configured)"
482+
fi
483+
484+
{
485+
printf '\n## tick at %s -- state=%s rc=%s sha=%s\n\n' \
486+
"$iso" "$state" "$tick_rc" "$capture_sha"
487+
printf 'PR: %s\n' "${DRIVER_PR:-(none)}"
488+
printf 'Workflow: %s\n\n' "${DRIVER_WORKFLOW:-unknown}"
489+
printf '### agent analysis\n\n'
490+
printf '%s\n' "$response"
491+
} >> "$improvements_file" 2>/dev/null || true
492+
}
493+
370494
# --- main loop ----------------------------------------------------------
371495

372496
run_once() {
@@ -455,13 +579,29 @@ main() {
455579
fi
456580

457581
# Outer loop. The -x transcript capture happens inside the loop body so
458-
# the transcript reflects the most recent tick's commands.
582+
# the transcript reflects the most recent tick's commands. Each tick's
583+
# own stdout+stderr also lands in a per-tick capture file whose SHA is
584+
# fed to an agent for self-improvement analysis (see capture_and_self_improve).
459585
while true; do
460586
# Each tick runs in a subshell with -x so the transcript captures
461587
# commands and arguments. The outer driver only re-reads its state
462-
# file between ticks.
463-
( set -x; run_once ) >> "$TRANSCRIPT" 2>&1
588+
# file between ticks. We capture the tick's combined stdout+stderr to
589+
# a per-tick file first, then append it to the rolling transcript so
590+
# the trap path still has the full history if anything goes wrong.
591+
local tick_capture
592+
tick_capture=$(mktemp "$TRANSCRIPT_DIR/driver-lane${LANE}-tick-XXXXXX.log")
593+
( set -x; run_once ) > "$tick_capture" 2>&1
464594
local rc=$?
595+
cat "$tick_capture" >> "$TRANSCRIPT"
596+
# Capture-and-self-improve happens after the state transition. The
597+
# agent invocation runs in the background so it does not block the
598+
# next tick; rc decisions below proceed without waiting. The
599+
# per-tick capture's content has already been hashed into the
600+
# journal object DB by the call below, so the tempfile is safe to
601+
# remove once the call returns (the background analyzer carries
602+
# only the SHA, not the path).
603+
capture_and_self_improve "$tick_capture" "$rc"
604+
rm -f "$tick_capture"
465605
case "$rc" in
466606
0) ;; # continue the loop
467607
2) DRIVER_EXPECTED_EXIT=1; break;; # terminal state reached

0 commit comments

Comments
 (0)