| 1 | |
| 2 | set -uo pipefail |
| 3 | PM="/shared/projects/zion-pm" |
| 4 | cd "$PM" || exit 1 |
| 5 | source "$PM/env.sh" |
| 6 | |
| 7 | TICK_INTERVAL="${PM_TICK_INTERVAL:-900}" |
| 8 | |
| 9 | read -r -d '' TICK_PROMPT <<'EOF' |
| 10 | Run ONE Zion-PM conductor tick. You are resume-safe; continue from disk state. |
| 11 | |
| 12 | Steps, in order, then stop: |
| 13 | 1. Read state/conductor_state.json, state/tasks.json, state/outbox.json. Read SOUL.md and |
| 14 | policy.yaml for your charter, autonomy gates, and the ≤4 concurrent-worker limit. |
| 15 | 2. Ingest any new files in inbox/ into state/tasks.json (status "open"), then delete those |
| 16 | inbox files. Each file's text is the task. |
| 17 | 3. For prioritization or any "what should I do" judgment, call homelab_context FIRST. Do not |
| 18 | sweep files. |
| 19 | 4. Pick the highest-leverage open/in-progress tasks (respect the concurrency cap). For each, |
| 20 | decide type (code|research|outreach|browser|ops|misc), the model per policy.yaml, and a |
| 21 | return contract. Spawn each via: bash bin/spawn-worker.sh <task-id> <type> "<scoped prompt>" |
| 22 | Mark dispatched tasks "in_progress" with their worker id. |
| 23 | 5. Drain: for any workers/*/result.json that are new, fold the result into the task |
| 24 | (status, artifacts, next_action). Write durable cross-project decisions to memory-graph. |
| 25 | 6. Gate: if a task's next step is outward-facing (send/post/push/deploy/contact/infra-mutate), |
| 26 | DO NOT do it. Append a fully-previewed item to state/outbox.json "pending" (id, kind, task, |
| 27 | preview with full content/recipient/target) and tell the task it is awaiting approval. |
| 28 | 7. Append a concise JSON line per material action to state/activity.jsonl. |
| 29 | 8. If and only if Zion is needed (an approval is pending, or a real blocker), push one ntfy |
| 30 | line to topic per policy.yaml. Otherwise stay silent. |
| 31 | 9. Update state/conductor_state.json (last_tick ts, ticks+1, running_workers). Stop. |
| 32 | |
| 33 | Be terse and frugal. This brain runs on a metered weekly cap - do the minimum tool calls |
| 34 | needed to move work forward. Never print secrets. Never execute an outbox item yourself. |
| 35 | EOF |
| 36 | |
| 37 | run_tick() { |
| 38 | echo "[pm] tick @ $(date -Is)" >&2 |
| 39 | claude -p \ |
| 40 | --dangerously-skip-permissions \ |
| 41 | --append-system-prompt "$(cat "$PM/SOUL.md")" \ |
| 42 | "$TICK_PROMPT" 2>>"$PM/logs/conductor.err" | tee -a "$PM/logs/conductor.out" |
| 43 | } |
| 44 | |
| 45 | case "${1:---once}" in |
| 46 | --once) |
| 47 | run_tick |
| 48 | ;; |
| 49 | --daemon) |
| 50 | if tmux has-session -t zion-pm 2>/dev/null; then |
| 51 | echo "zion-pm already running"; exit 0 |
| 52 | fi |
| 53 | tmux new-session -d -s zion-pm \ |
| 54 | "while true; do PM_BRAIN='${PM_BRAIN:-glm}' bash '$PM/bin/conductor.sh' --once; sleep $TICK_INTERVAL; done" |
| 55 | echo "started zion-pm conductor loop (tick=${TICK_INTERVAL}s, brain=${PM_BRAIN:-glm})" |
| 56 | ;; |
| 57 | *) |
| 58 | echo "usage: conductor.sh --once|--daemon" >&2; exit 1 |
| 59 | ;; |
| 60 | esac |