--- name: setup-agents-task-runner version: 0.1.0 description: 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 `` 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: Install dirs: + () Services: agents-task-runner, -watchdog, -appeals-inbox (run-as-user: , NOT root) Legacy teardown: Scope file: )> winsw (Win only): fetch v2.12.0 WinSW-x64.exe, verify SHA256= Run-as password: 's password (run-as-user requirement)> Backups: existing unit/config files → .bak- ``` 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 `.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. ```bash # 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 /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//`, 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):** ```bash mkdir -p ~/.config/systemd/user cp /*.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):** ```bash cp /*.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. ```powershell # 1. Fetch + verify (ABORT on mismatch; STOP if pin is still the placeholder) Invoke-WebRequest -OutFile "$INSTALL_DIR\winsw.exe" if ((Get-FileHash "$INSTALL_DIR\winsw.exe" -Algorithm SHA256).Hash -ne $ExpectedSha) { throw "winsw SHA256 mismatch" } # 2. winsw convention: .exe + .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) ```bash 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 services, run-as-user , DISARMED. Services: agents-task-runner (:3000), -watchdog, -appeals-inbox (:4317) Install dirs: + (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: .bak-. 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\.exe" stop; & "$INSTALL_DIR\.exe" uninstall` per id 2. Restore any `.bak-` 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 `.xml` | `%LOCALAPPDATA%\agents-task-runner\` | `` + 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).