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>
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-runnerstays editable, and editing the dev tree does NOT hot-patch the running service. Stops at confirmation gates — it installs OS services, fetcheswinsw.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.ps1Scheduled 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'ssetup-projects-meta) — this skill uses itsdist/tasks-cli.js. - docker
mongo+reconcilerbring-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_PROJECTSorDRY_RUNinto 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 ofservice/templates + the runner/watchdog) and~/projects/.common/lib/appeals-inbox/. projects-meta-mcpbuilt:~/projects/.common/lib/projects-meta-mcp/dist/tasks-cli.jsexists (→{{TASKS_BIN}}). If missing → runsetup-projects-metafirst; 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*(orGet-Service agents-task-runner*)
- Linux:
- Legacy launcher. Windows Scheduled Task
AgentsTaskRunnerWorker(thestart-worker.ps1task) — 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-emptyarmed[])? If armed, record and DO NOT touch. - winsw pin (Windows only). Read
service/winsw/WINSW-PIN.md— isexpected SHA256filled (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:
- Starts without a window. No console window appears;
services.msc/systemctl --user status/launchctl listshows the three running. - Survives kill. Kill the task-runner PID; within the restart window the OS supervisor respawns it (re-check status / port 3000 answers again).
- 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. - 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
- 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" uninstallper id
- Linux:
- Restore any
.bak-<ts>files. - Re-enable the legacy launcher only if you need the old path back:
schtasks /change /tn AgentsTaskRunnerWorker /enable. - Install dirs are disposable copies —
rm -rfthem; the dev tree is untouched. - Leave
poller-scope.jsonas-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 spawnsclaude— 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).