From 9954356a4aace8c0b793b7c2fe65e8a3fb62fdb1 Mon Sep 17 00:00:00 2001 From: vitya Date: Tue, 16 Jun 2026 19:14:40 +0300 Subject: [PATCH] feat(setup-agents-task-runner): L2 installer skill for standing-duty service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- dist/setup-agents-task-runner.skill | Bin 0 -> 6502 bytes skills/setup-agents-task-runner/README.md | 16 ++ skills/setup-agents-task-runner/SKILL.md | 252 ++++++++++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 dist/setup-agents-task-runner.skill create mode 100644 skills/setup-agents-task-runner/README.md create mode 100644 skills/setup-agents-task-runner/SKILL.md diff --git a/dist/setup-agents-task-runner.skill b/dist/setup-agents-task-runner.skill new file mode 100644 index 0000000000000000000000000000000000000000..7c79f07800e10340dbb429ce9d29cf1ae9c23798 GIT binary patch literal 6502 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 L&tFIQukJqpD?EYC literal 0 HcmV?d00001 diff --git a/skills/setup-agents-task-runner/README.md b/skills/setup-agents-task-runner/README.md new file mode 100644 index 0000000..84c0441 --- /dev/null +++ b/skills/setup-agents-task-runner/README.md @@ -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. diff --git a/skills/setup-agents-task-runner/SKILL.md b/skills/setup-agents-task-runner/SKILL.md new file mode 100644 index 0000000..e8ad4b5 --- /dev/null +++ b/skills/setup-agents-task-runner/SKILL.md @@ -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 + `` 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).