Skip to content

Commit 48afa74

Browse files
author
endolinbot
committed
feat(cleaner): skeleton worker for the script-orchestrated worker-pool (#3)
Phase-1 demonstration that the worker-pool model from designs/driver.md works end to end. The skeleton accepts a job path under either the flat job board (jobs/{open,claimed}/<...>.md with eligible_roles containing cleaner) or the per-role board (jobs/cleaner/{open,claimed}/<...>.md), moves the job to a matching done/ or abandoned/ directory, and appends a completion stamp (matching skills/job-board/complete-job.sh's format). Phase-3 lands the real coverage-driven-testing body inside cleaner.sh; phase 1 leaves it as a no-op so the wiring can be exercised without dragging in the full cleaner machinery. The CLEANER_WORK_HOOK env var lets the self-test inject a deterministic outcome. A SKILL.md sits alongside (per the garden's roles-vs-skills convention). A self-test at skills/cleaner/test-cleaner.sh builds a mock journal in a tempdir, posts one job in each board shape, runs the cleaner on each, and asserts the resulting done-file has the completion stamp. Removing the mv line from cleaner.sh causes the test to fail (regression evidence per skills/regression-evidence/SKILL.md). This addresses the maintainer's directive in the PR #3 review body ('skeleton per-role executable proving the worker-pool model end to end'). The concurrent builder dispatch that landed the driver script did not include a per-role worker; this fills that gap. Both board shapes are accepted because the design names per-role boards as the layout but the current driver.sh implementation uses the flat board with an --eligible filter. A surfaced PR-comment names the maintainer-decision point; the script supports either path until that converges. Passes bash -n and shellcheck 0.9.0. The self-test passes.
1 parent 661ad78 commit 48afa74

3 files changed

Lines changed: 256 additions & 0 deletions

File tree

skills/cleaner/SKILL.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
created: 2026-06-01
3+
updated: 2026-06-01
4+
author: builder
5+
---
6+
7+
# Skill: cleaner
8+
9+
The cleaner's per-role executable for the script-orchestrated worker-pool model from [`designs/driver.md`](../../designs/driver.md).
10+
Phase 1 ships a skeleton that demonstrates the worker-pool handshake end to end: a job posted to either board shape (flat `jobs/{open,claimed}/` or per-role `jobs/cleaner/{open,claimed}/`) is picked up and moved to the matching `done/` with a completion stamp.
11+
12+
The role's full operating brief is at [`roles/cleaner/AGENT.md`](../../roles/cleaner/AGENT.md).
13+
This skill is the bash-process side of the cleaner role; the AGENT.md is the LLM-subagent side.
14+
Phase 3 lands the real coverage-driven-testing body inside this script; phase 1 leaves it as a no-op so the worker-pool wiring can be exercised end to end without dragging in the full cleaner machinery.
15+
16+
## When to use
17+
18+
- The driver posts a `clean` job (per `skills/driver-state-machine/SKILL.md` § `clean` state).
19+
- A worker daemon polling either the flat board (`jobs/open/` with `eligible_roles:` containing `cleaner`) or the per-role board (`jobs/cleaner/open/`) claims the job and invokes this script with the claimed path.
20+
- The script does the work (phase 1: no-op stub; phase 3: coverage-driven-testing pass) and moves the job to `done/` or `abandoned/`.
21+
22+
## Inputs
23+
24+
`cleaner.sh <job-path>` where `<job-path>` is relative to the journal worktree (e.g. `jobs/open/<UTC>--<short-id>--<slug>.md` or `jobs/cleaner/open/<UTC>--<short-id>--<slug>.md`).
25+
26+
`GARDEN_ROLE=cleaner` should be set by the caller (the worker daemon's bootstrap; the skeleton tolerates it being unset but expects the caller to set it for downstream identity discipline).
27+
28+
Environment overrides:
29+
30+
- `GARDEN_ROOT`: defaults to the script-location-relative parent (`skills/cleaner/../..`).
31+
- `CLEANER_JOURNAL_DIR`: defaults to `$GARDEN_ROOT/journal`. The test harness overrides this to a mock journal tree.
32+
- `CLEANER_WORK_HOOK`: optional executable invoked with the job's full path; if it exits non-zero the job moves to `abandoned/` instead of `done/`. Used by the test harness to exercise the negative path.
33+
34+
## Output
35+
36+
- The job file is moved from its `open/` or `claimed/` directory to the matching `done/` or `abandoned/` directory under the same board prefix.
37+
- The script prints the destination path (relative to the journal worktree) on stdout.
38+
- Exit code 0 on `done`, 1 on `abandoned`, 64 on usage error.
39+
40+
The script does **not** commit-and-push the move. The supervisor or driver is responsible for the journal sync cadence (the existing `skills/job-board/job-board-poll.sh` daemon handles the read side; the cleaner's caller handles the write side after the worker returns).
41+
42+
## Both board shapes
43+
44+
Phase 1 supports both shapes because the design names per-role boards as the layout but the current driver implementation uses the flat board with an `eligible_roles:` filter:
45+
46+
- **Flat board**: `jobs/open/<...>.md` with `eligible_roles: [- cleaner]`. The cleaner worker is one of several roles that may claim from `jobs/open/`; the filter prevents non-cleaner workers from claiming.
47+
- **Per-role board**: `jobs/cleaner/open/<...>.md`. Only cleaner workers poll `jobs/cleaner/open/`.
48+
49+
The skill accepts either path on argv; the maintainer's preferred layout is a design-level decision the script can follow once it converges.
50+
51+
## Self-test
52+
53+
A quick self-test sits next to the script at [`test-cleaner.sh`](./test-cleaner.sh).
54+
It builds a tiny mock journal in a tempdir, posts one job in each board shape, runs the cleaner on each, and asserts the resulting done-file has the completion stamp.
55+
56+
```sh
57+
skills/cleaner/test-cleaner.sh
58+
```
59+
60+
Exit code 0 iff both shapes pass.
61+
62+
## Notes from the field
63+
64+
(Append; terse and dated.)
65+
66+
- _2026-06-01_: initial skeleton landed as part of phase 1 of the driver migration (PR #3 § Migration plan). Phase 3 lands the real coverage-driven-testing body; until then `CLEANER_WORK_HOOK` is the only way to exercise the negative path.

skills/cleaner/cleaner.sh

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#!/usr/bin/env bash
2+
# cleaner.sh — skeleton per-role worker for the cleaner role.
3+
#
4+
# This is the phase-1 demonstration of the worker-pool model from
5+
# designs/driver.md. A real cleaner worker (phase 3) will perform the
6+
# coverage-driven-testing pass, push tests, run CI, etc. This skeleton
7+
# implements only the worker-pool handshake:
8+
#
9+
# 1. Read a job's path on argv ($1 = either the flat-board path
10+
# `jobs/{open,claimed}/<...>.md` with `eligible_roles: [- cleaner]`,
11+
# or the per-role-board path `jobs/cleaner/{open,claimed}/<...>.md`).
12+
# 2. Read the body, do a no-op "work" pass (the phase-3 body lands here).
13+
# 3. Move the job to a `done/` directory matching the source board
14+
# with a one-line completion stamp.
15+
#
16+
# Both board shapes are accepted so this skeleton fits the flat-board
17+
# driver (designs/driver.md's PR-creation-flow workflow) and the
18+
# per-role-board driver (the design's Role-specific job boards section).
19+
# A real worker pool would have the workers race for `open/` via
20+
# `skills/job-board/claim-job.sh` (or a future per-role variant) before
21+
# invoking this script with the resulting `claimed/` path.
22+
#
23+
# Usage:
24+
# GARDEN_ROLE=cleaner cleaner.sh <job-path>
25+
#
26+
# Where <job-path> is relative to the journal worktree.
27+
#
28+
# Exit codes:
29+
# 0: work succeeded; job moved to a done/ directory
30+
# 1: work failed; job moved to an abandoned/ directory
31+
# 64: usage error
32+
33+
set -uo pipefail
34+
35+
if [ "$#" -ne 1 ]; then
36+
echo "usage: GARDEN_ROLE=cleaner $0 <job-path>" >&2
37+
exit 64
38+
fi
39+
40+
SOURCE=$1
41+
42+
# Resolve the journal worktree. The skill ships under skills/cleaner/ in
43+
# the garden tree; the journal is the sibling worktree at $GARDEN_ROOT/journal.
44+
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
45+
GARDEN_ROOT=${GARDEN_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}
46+
JRN=${CLEANER_JOURNAL_DIR:-$GARDEN_ROOT/journal}
47+
48+
# Determine the board prefix (`jobs/` for the flat board, `jobs/cleaner/`
49+
# for the per-role board). The completion stamp will land in the same
50+
# board's done/ or abandoned/ directory.
51+
case "$SOURCE" in
52+
jobs/open/*.md|jobs/claimed/*.md)
53+
BOARD_PREFIX="jobs"
54+
;;
55+
jobs/cleaner/open/*.md|jobs/cleaner/claimed/*.md)
56+
BOARD_PREFIX="jobs/cleaner"
57+
;;
58+
*)
59+
echo "cleaner: source must be under jobs/{open,claimed}/ or jobs/cleaner/{open,claimed}/ (got $SOURCE)" >&2
60+
exit 64
61+
;;
62+
esac
63+
64+
test -f "$JRN/$SOURCE" || { echo "cleaner: $SOURCE not found under $JRN" >&2; exit 1; }
65+
66+
# Parse the short-id from the filename. The flat-board open/ name is
67+
# <UTC>--<short-id>--<slug>.md; the claimed/ name is
68+
# <UTC>--<host>--<role>--<sid>--<short-id>--<slug>.md.
69+
BASE=$(basename "$SOURCE" .md)
70+
case "$SOURCE" in
71+
*/open/*) SHORT=$(printf '%s' "$BASE" | awk -F-- '{print $2}') ;;
72+
*/claimed/*) SHORT=$(printf '%s' "$BASE" | awk -F-- '{print $5}') ;;
73+
*) SHORT= ;;
74+
esac
75+
if [ -z "$SHORT" ]; then
76+
echo "cleaner: cannot parse short-id from $BASE" >&2
77+
exit 1
78+
fi
79+
80+
# Phase-1 "work" is a no-op. Phase-3 lands the real coverage-driven-testing
81+
# loop here. The hook env var CLEANER_WORK_HOOK lets the test harness
82+
# inject a deterministic outcome.
83+
OUTCOME="done"
84+
if [ -n "${CLEANER_WORK_HOOK:-}" ]; then
85+
if ! "$CLEANER_WORK_HOOK" "$JRN/$SOURCE"; then
86+
OUTCOME="abandoned"
87+
fi
88+
fi
89+
90+
# Move the job to done/ (or abandoned/). We do not commit-and-push from
91+
# the skeleton; the supervisor or driver does that.
92+
UTC=$(date -u +%Y%m%dT%H%M%SZ)
93+
DEST_REL="$BOARD_PREFIX/$OUTCOME/${UTC}--${BASE#*--}.md"
94+
mkdir -p "$JRN/$(dirname "$DEST_REL")"
95+
96+
mv "$JRN/$SOURCE" "$JRN/$DEST_REL"
97+
98+
# Append a completion stamp at the body's end (matching skills/job-board/complete-job.sh).
99+
{
100+
cat "$JRN/$DEST_REL"
101+
printf '\n# Completion stamp\n'
102+
printf 'completed_at: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
103+
printf 'outcome: %s\n' "$OUTCOME"
104+
printf 'worker: cleaner.sh (skeleton)\n'
105+
} > "$JRN/$DEST_REL.tmp" && mv "$JRN/$DEST_REL.tmp" "$JRN/$DEST_REL"
106+
107+
printf '%s\n' "$DEST_REL"
108+
109+
# Skeleton's exit code mirrors the work outcome.
110+
test "$OUTCOME" = "done" || exit 1

skills/cleaner/test-cleaner.sh

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env bash
2+
# test-cleaner.sh — quick self-test for the cleaner skeleton worker.
3+
#
4+
# Builds a tiny mock journal in a tempdir, posts a flat-board cleaner job
5+
# and a per-role-board cleaner job, runs cleaner.sh on each, and asserts
6+
# the resulting done/ files exist with the expected completion stamp.
7+
#
8+
# Regression evidence: removing the `mv` line in cleaner.sh causes the
9+
# done-file assertion to fail; that breakage was used during authoring
10+
# to confirm the test is load-bearing.
11+
#
12+
# Usage:
13+
# skills/cleaner/test-cleaner.sh
14+
#
15+
# Exit code 0 iff both board shapes complete cleanly.
16+
17+
set -uo pipefail
18+
19+
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
20+
CLEANER_SH=$SCRIPT_DIR/cleaner.sh
21+
22+
MOCK=$(mktemp -d -t cleaner-test-XXXXXX)
23+
trap 'rm -rf "$MOCK"' EXIT
24+
25+
mkdir -p "$MOCK/jobs/open" "$MOCK/jobs/claimed" "$MOCK/jobs/done" "$MOCK/jobs/abandoned"
26+
mkdir -p "$MOCK/jobs/cleaner/open" "$MOCK/jobs/cleaner/claimed" \
27+
"$MOCK/jobs/cleaner/done" "$MOCK/jobs/cleaner/abandoned"
28+
29+
post_job() {
30+
local rel=$1
31+
cat > "$MOCK/$rel" <<EOF
32+
---
33+
job: abc123
34+
posted_by_role: driver
35+
verb: clean
36+
eligible_roles:
37+
- cleaner
38+
---
39+
40+
A demo cleaner job.
41+
EOF
42+
}
43+
44+
run_case() {
45+
local label=$1
46+
local source_rel=$2
47+
post_job "$source_rel"
48+
if ! out=$(GARDEN_ROLE=cleaner CLEANER_JOURNAL_DIR="$MOCK" "$CLEANER_SH" "$source_rel"); then
49+
echo "FAIL $label: cleaner.sh exited non-zero" >&2
50+
return 1
51+
fi
52+
if [ ! -f "$MOCK/$out" ]; then
53+
echo "FAIL $label: done-file missing at $out" >&2
54+
return 1
55+
fi
56+
if [ -f "$MOCK/$source_rel" ]; then
57+
echo "FAIL $label: source still present at $source_rel" >&2
58+
return 1
59+
fi
60+
if ! grep -q "outcome: done" "$MOCK/$out"; then
61+
echo "FAIL $label: completion stamp missing 'outcome: done'" >&2
62+
return 1
63+
fi
64+
if ! grep -q "worker: cleaner.sh" "$MOCK/$out"; then
65+
echo "FAIL $label: completion stamp missing worker line" >&2
66+
return 1
67+
fi
68+
return 0
69+
}
70+
71+
# Flat-board (jobs/open/) shape.
72+
SHORT_FLAT=abc123
73+
FLAT_REL="jobs/open/20260601T120000Z--${SHORT_FLAT}--demo.md"
74+
run_case "flat-board" "$FLAT_REL" || exit 1
75+
76+
# Per-role-board (jobs/cleaner/open/) shape.
77+
ROLE_REL="jobs/cleaner/open/20260601T120000Z--abc124--demo.md"
78+
run_case "per-role-board" "$ROLE_REL" || exit 1
79+
80+
echo "pass cleaner skeleton handles both board shapes"

0 commit comments

Comments
 (0)