Files
claude-skills/skills/setup-agents-task-runner/SKILL.md
vitya 9954356a4a feat(setup-agents-task-runner): L2 installer skill for standing-duty service
poller-service-deploy-via-factory (fork 1). New skill v0.1.0 — installs the
standing-duty stack as platform-native services (systemd/launchd/winsw):
no node window, OS-supervised autostart+crash-restart, run-as-user, deploy-boundary.

- fetches winsw (pinned v2.12.0 + SHA256-verify, not vendored; STOP on placeholder)
- installs DISARMED: scope is runtime config (poller-scope.json), arming is a
  separate operator step via the appeals-inbox pult; never carries POLLER_PROJECTS/DRY_RUN
- confirmation gates: discovery (read-only) -> plan -> writes; rollback section
- tears down the legacy start-worker.ps1 Scheduled Task (no double-claim)
- dist/setup-agents-task-runner.skill rebuilt

[skip-tdd: visual] — installer docs + OS service config, no testable pure logic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 10:28:47 +03:00

14 KiB

name, version, description
name version description
setup-agents-task-runner 0.1.0 Installs the standing-duty stack (agents-task-runner + watchdog + appeals-inbox) as platform-native OS services — systemd user units on Linux, launchd LaunchAgents on macOS, winsw-wrapped services on Windows. No node window on any OS; OS-supervised autostart + crash-restart. Fetches winsw (pinned + SHA256-verified, not vendored). Installs DISARMED — scope is runtime config (poller-scope.json), arming is a separate operator step via the appeals-inbox pult. Use when the user says "install agents-task-runner service", "set up the standing-duty service", "deploy the poller as a service", "настрой службу раннера", "поставь дежурный стек как службу", "agents-task-runner службой", or when migrating off the old start-worker.ps1 Scheduled Task. Cross-platform — Windows / Linux / macOS. Installs OS services, fetches a binary, writes a scope file; pauses for confirmation before every mutating phase. This is the L2 installer for the `agents-task-runner` factory module.

setup-agents-task-runner

One-time L2 installer that turns the standing-duty stack into platform-native OS services with a hard deploy-boundary: the service runs from a factory-install copy, the dev tree .common/lib/agents-task-runner stays editable, and editing the dev tree does NOT hot-patch the running service. Stops at confirmation gates — it installs OS services, fetches winsw.exe, and writes a runtime scope file.

Design: concepts/poller-standing-duty (fork 1, OpeItcLoc03/common). Service templates live in OpeItcLoc03/common @ lib/agents-task-runner/service/ (README.md is the launch-recipe SSOT). This skill is the agents-task-runner module declared in ~/.factory/factory.yaml.

The three services

mongo + reconciler stay in docker (own restart policy). This skill installs only the host node processes (LocalSpawnAdapter spawns the host claude, which docker can't):

service id script port role
agents-task-runner task-runner/server.js 3000 claim + spawn
agents-task-runner-watchdog watchdog/watchdog.js hang-backstop + board hygiene
agents-task-runner-appeals-inbox dist/index.js 4317 HITL pult + arming control

Two-level supervision: OS supervisor = crash/exit restart (primary); watchdog = alive-but-hung backstop + board hygiene. Both kept — different failure modes, not duplicates.

When to use

  • User explicitly asks to install / set up / deploy the agents-task-runner (or "standing-duty") service.
  • Migrating off the legacy start-worker.ps1 Scheduled Task (the live-patch-prone launcher this replaces).
  • New machine in the fleet that should run standing duty.

Out of scope

  • Arming / going-live. This skill installs the stack disarmed. Arming a project for autonomous spawn is a runtime operator step via the appeals-inbox pult (writes poller-scope.json). Never arm from this skill.
  • Editing runner / watchdog / appeals-inbox source — that's dev-tree work in OpeItcLoc03/common.
  • Building / registering projects-meta-mcp (that's setup-projects-meta) — this skill uses its dist/tasks-cli.js.
  • docker mongo + reconciler bring-up (docker compose -f docker-compose.yml -f docker-compose.host.yml up -d).
  • Pushing any repo.

Hard rule: don't auto-mutate

The procedure copies a deploy tree, fetches and runs a binary, installs OS services, and writes a scope file. Pause for explicit confirmation between Phase 1 (discovery, read-only) and Phase 2 (plan), and again before Phase 3+ (writes). A trigger phrase authorizes discovery only.

Two never-do guardrails:

  • Never carry POLLER_PROJECTS or DRY_RUN into any unit — scope is runtime config now. Their presence is the exact anti-pattern this deploy removes.
  • Never overwrite an existing armed poller-scope.json. If it exists, leave it. Only create a disarmed {"armed":[]} when absent.

Procedure

Phase 0 — Environment sanity (read-only)

  • Node ≥ 22 on PATH (node --version); capture the absolute node binary → {{NODE_BIN}}.
  • Dev tree present: ~/projects/.common/lib/agents-task-runner/ (source of service/ templates + the runner/watchdog) and ~/projects/.common/lib/appeals-inbox/.
  • projects-meta-mcp built: ~/projects/.common/lib/projects-meta-mcp/dist/tasks-cli.js exists (→ {{TASKS_BIN}}). If missing → run setup-projects-meta first; stop.
  • Resolve {{HOME}}, {{USER}}, {{PROJECTS_ROOT}} (~/projects).
  • Detect OS → systemd (Linux) / launchd (macOS) / winsw (Windows).

Phase 1 — Discovery (read-only)

Report "found / absent" for each; never echo secrets:

  • Install dirs. Default {{INSTALL_DIR}} / {{APPEALS_DIR}} per OS (Phase 2 table). Note if they already exist (→ redeploy, not first install).
  • Existing services.
    • Linux: systemctl --user list-unit-files 'agents-task-runner*'
    • macOS: ls ~/Library/LaunchAgents/site.kzntsv.agents-task-runner*
    • Windows: sc.exe query agents-task-runner* (or Get-Service agents-task-runner*)
  • Legacy launcher. Windows Scheduled Task AgentsTaskRunnerWorker (the start-worker.ps1 task) — flag it for teardown in Phase 2 (it must not coexist with the service — two task-runners = double-claim).
  • Scope file. ~/.config/projects-mcp/poller-scope.json — present? armed (non-empty armed[])? If armed, record and DO NOT touch.
  • winsw pin (Windows only). Read service/winsw/WINSW-PIN.md — is expected SHA256 filled (not the <FILL-FROM-RELEASE> placeholder)? If placeholder → Phase 2 must STOP and ask the operator to fill it.

Phase 2 — Plan + confirm

Present one block. Default install dirs:

OS {{INSTALL_DIR}} {{APPEALS_DIR}} service mechanism
Linux ~/.local/share/agents-task-runner ~/.local/share/appeals-inbox systemd --user
macOS ~/Library/Application Support/agents-task-runner ~/Library/Application Support/appeals-inbox launchd LaunchAgents
Windows %LOCALAPPDATA%\agents-task-runner %LOCALAPPDATA%\appeals-inbox winsw
OS / mechanism:   <systemd | launchd | winsw>
Install dirs:     <INSTALL_DIR> + <APPEALS_DIR>   (<first install | redeploy over existing>)
Services:         agents-task-runner, -watchdog, -appeals-inbox   (run-as-user: <USER>, NOT root)
Legacy teardown:  <Scheduled Task AgentsTaskRunnerWorker → disable | none>
Scope file:       <create disarmed {"armed":[]} | exists, leave untouched (armed=<n>)>
winsw (Win only): fetch v2.12.0 WinSW-x64.exe, verify SHA256=<filled | PLACEHOLDER → STOP>
Run-as password:  <Windows: will prompt for <USER>'s password (run-as-user requirement)>
Backups:          existing unit/config files → <file>.bak-<ts>

Wait for explicit "ok / go / поехали". State plainly: this installs disarmed; nothing spawns until you arm a project via the pult.

Phase 3 — Backup

Copy any existing unit / plist / winsw config that will be overwritten to <file>.bak-YYYYMMDD-HHMMSS. Deploy copies need no backup (git is the backup).

Phase 4 — Deploy copy (the boundary)

Sync the dev tree into the install dirs — the service runs from here, NOT the dev tree.

# runner (+ watchdog, which lives inside it)
rsync -a --delete --exclude node_modules ~/projects/.common/lib/agents-task-runner/ "$INSTALL_DIR"/   # or robocopy /MIR on Windows
( cd "$INSTALL_DIR" && npm ci --omit=dev )

# appeals-inbox (build dist)
rsync -a --delete --exclude node_modules ~/projects/.common/lib/appeals-inbox/ "$APPEALS_DIR"/
( cd "$APPEALS_DIR" && npm ci && npm run build )   # produces dist/index.js

Windows: use robocopy <src> <dst> /MIR /XD node_modules instead of rsync. Verify "$INSTALL_DIR"/task-runner/server.js, "$INSTALL_DIR"/watchdog/watchdog.js, and "$APPEALS_DIR"/dist/index.js exist before proceeding.

Phase 5 — Render templates

For each unit in service/<systemd|launchd|winsw>/, substitute the placeholders ({{NODE_BIN}}, {{INSTALL_DIR}}, {{APPEALS_DIR}}, {{HOME}}, {{USER}}, {{TASKS_BIN}}, {{PROJECTS_ROOT}}; Windows also {{WINSW_USER_PASSWORD}}) → rendered files. Create the log dirs the units reference (~/.local/state/agents-task-runner/, ~/Library/Logs/agents-task-runner/, or %LOCALAPPDATA%\agents-task-runner\logs). Confirm no {{...}} token remains in any rendered file.

Phase 6 — Install services

Linux (systemd user):

mkdir -p ~/.config/systemd/user
cp <rendered>/*.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now agents-task-runner-appeals-inbox.service \
                              agents-task-runner.service \
                              agents-task-runner-watchdog.service
loginctl enable-linger "$USER"   # survive logout / start at boot

macOS (launchd):

cp <rendered>/*.plist ~/Library/LaunchAgents/
for p in site.kzntsv.agents-task-runner-appeals-inbox site.kzntsv.agents-task-runner site.kzntsv.agents-task-runner-watchdog; do
  launchctl unload ~/Library/LaunchAgents/$p.plist 2>/dev/null
  launchctl load -w ~/Library/LaunchAgents/$p.plist
done

Windows (winsw): follow service/winsw/WINSW-PIN.md verification contract first.

# 1. Fetch + verify (ABORT on mismatch; STOP if pin is still the placeholder)
Invoke-WebRequest <pinned-url> -OutFile "$INSTALL_DIR\winsw.exe"
if ((Get-FileHash "$INSTALL_DIR\winsw.exe" -Algorithm SHA256).Hash -ne $ExpectedSha) { throw "winsw SHA256 mismatch" }
# 2. winsw convention: <id>.exe + <id>.xml side by side. Copy winsw.exe per service id, place rendered xml.
#    Then install + start each:
& "$INSTALL_DIR\agents-task-runner.exe" install
& "$INSTALL_DIR\agents-task-runner.exe" start
# repeat for -watchdog and -appeals-inbox

Disable the legacy launcher so it can't coexist: schtasks /change /tn AgentsTaskRunnerWorker /disable (or /delete after confirming the service is healthy).

Phase 7 — Scope file (disarmed default)

mkdir -p ~/.config/projects-mcp
# Only if absent — NEVER overwrite an existing (possibly armed) file:
[ -f ~/.config/projects-mcp/poller-scope.json ] || echo '{"armed":[]}' > ~/.config/projects-mcp/poller-scope.json

Phase 8 — Verify acceptance

The design's acceptance criteria — verify each, show evidence:

  1. Starts without a window. No console window appears; services.msc / systemctl --user status / launchctl list shows the three running.
  2. Survives kill. Kill the task-runner PID; within the restart window the OS supervisor respawns it (re-check status / port 3000 answers again).
  3. Reads scope from runtime config. With {"armed":[]} the poller logs claim nothing (disarmed). Optionally arm a throwaway entry in the scope file and confirm hot-reload picks it up WITHOUT a restart (then revert) — but real arming is the operator's pult step, not this skill's.
  4. No POLLER_PROJECTS / DRY_RUN present in any installed unit (grep the rendered files).

Phase 9 — Final report

✅ Standing-duty stack installed as <mechanism> services, run-as-user <USER>, DISARMED.
   Services: agents-task-runner (:3000), -watchdog, -appeals-inbox (:4317)
   Install dirs: <INSTALL_DIR> + <APPEALS_DIR>  (dev tree stays editable — deploy-boundary)
   Scope: ~/.config/projects-mcp/poller-scope.json = {"armed":[]} (nothing spawns yet)

   GOING LIVE is a separate operator step: arm a project via the appeals-inbox pult
   (http://127.0.0.1:4317). Until then the poller claims nothing.

   Redeploy after a dev-tree change: re-run this skill (re-syncs install dir + restarts),
   or `factory update agents-task-runner` once the L1 Go-CLI lands. Editing the dev tree
   does NOT hot-patch the running service.

   Backups: <files>.bak-<ts>. docker mongo+reconciler are separate — bring up via compose.

Rollback

  1. Stop + remove the services:
    • Linux: systemctl --user disable --now agents-task-runner*.service; rm ~/.config/systemd/user/agents-task-runner*.service; systemctl --user daemon-reload
    • macOS: launchctl unload ~/Library/LaunchAgents/site.kzntsv.agents-task-runner*.plist; rm ...
    • Windows: & "$INSTALL_DIR\<id>.exe" stop; & "$INSTALL_DIR\<id>.exe" uninstall per id
  2. Restore any .bak-<ts> files.
  3. Re-enable the legacy launcher only if you need the old path back: schtasks /change /tn AgentsTaskRunnerWorker /enable.
  4. Install dirs are disposable copies — rm -rf them; the dev tree is untouched.
  5. Leave poller-scope.json as-is.

Cross-platform notes

service unit install location run-as-user boot-before-login
Linux systemd *.service ~/.config/systemd/user/ inherent (user unit) loginctl enable-linger
macOS launchd *.plist ~/Library/LaunchAgents/ inherent (LaunchAgent) runs at login (Agent)
Windows winsw <id>.xml %LOCALAPPDATA%\agents-task-runner\ <serviceaccount> + password needs stored creds; login-triggered is acceptable on a personal box

Common mistakes

  • Skipping Phase 1. Re-installing over an existing armed scope file or a running service without noticing → double-claim or a clobbered arming state.
  • Carrying POLLER_PROJECTS / DRY_RUN. The whole point is runtime scope. Grep the rendered units.
  • Leaving the Scheduled Task enabled alongside the service. Two task-runners claim the same board → double-claim. Disable the legacy launcher.
  • Running as root / LocalSystem. The runner needs the user's ~/.config, ~/.claude, git creds and spawns claude — must be the user account.
  • Fabricating / skipping the winsw SHA256. STOP if the pin is the placeholder; abort on mismatch.
  • Treating install as going-live. Installed ≠ armed. Nothing spawns until the operator arms via the pult.
  • Editing the dev tree and expecting the service to pick it up. It won't — redeploy (re-sync + restart).