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>
This commit is contained in:
2026-06-16 19:14:40 +03:00
parent b0b0c49cd9
commit 9954356a4a
3 changed files with 268 additions and 0 deletions

BIN
dist/setup-agents-task-runner.skill vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,16 @@
# setup-agents-task-runner
L2 installer skill for the **standing-duty stack** — turns `agents-task-runner` + `watchdog` +
`appeals-inbox` into platform-native OS services (systemd / launchd / winsw): no node window,
OS-supervised autostart + crash-restart, run-as-user, hard deploy-boundary.
- **Design:** `concepts/poller-standing-duty` (fork 1), OpeItcLoc03/common.
- **Service templates:** `OpeItcLoc03/common @ lib/agents-task-runner/service/`.
- **Factory module:** `agents-task-runner` in `~/.factory/factory.yaml`.
Installs **disarmed** — scope is runtime config (`~/.config/projects-mcp/poller-scope.json`); arming a
project for autonomous spawn is a separate operator step via the appeals-inbox pult. Cross-platform.
Confirmation gates before every mutating phase (copies a deploy tree, fetches `winsw.exe`
pinned+SHA256-verified, installs OS services).
See `SKILL.md` for the full procedure.

View File

@@ -0,0 +1,252 @@
---
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
`<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.
```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 <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):**
```bash
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):**
```bash
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.
```powershell
# 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)
```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 <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).