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:
BIN
dist/setup-agents-task-runner.skill
vendored
Normal file
BIN
dist/setup-agents-task-runner.skill
vendored
Normal file
Binary file not shown.
16
skills/setup-agents-task-runner/README.md
Normal file
16
skills/setup-agents-task-runner/README.md
Normal 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.
|
||||||
252
skills/setup-agents-task-runner/SKILL.md
Normal file
252
skills/setup-agents-task-runner/SKILL.md
Normal 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).
|
||||||
Reference in New Issue
Block a user