AGENTS.md@packages/automation-plugin/src/server · git:20260819.0abd537 · 2026-08-19 · sha256 4a9336092baee58b
AGENTS.md@packages/automation-plugin/src/server git:20260819.0abd537A
Immutable. This exact content is served forever at /api/v1/blob/4a9336092baee58b.
# DOX — packages/automation-plugin/src/server
Files in this directory. One row per source file.
| File | Purpose |
|------|---------|
| `action-registry.ts` | Extensible automation action registry. `ActionRegistry` register/get/has/ids/descriptorsForCwd. Namespaced `<source>.<verb>` ids. Per-source cap MAX_PER_SOURCE=12. `createActionRegistryWithBuiltins` seeds `core.prompt`/`core.skill`. `normalizeActionKind` maps bare prompt/skill→core.*. Descriptor resolves available(cwd)+enum options. Action `buildPrompt(payload,automation)` produces run seed prompt. See change: register-plugin-automation-events. ActionRegistration adds optional `buildEvent` (event dispatch) alongside optional `buildPrompt`; register guard requires exactly one. See change: automation-emit-configured-event. Adds `coreActionContributions()`, `collectActionRegistry(entries)`, `ACTION_CONTRIBUTION_PREFIX`. Registry built by collecting published contributions. See change: decouple-automation-action-registry. `ActionEvent` gains optional `completion {eventType, summarize?}` (+ `ActionCompletion` type) so an event-dispatch action declares how a run of it FINISHES; the automation engine finalizes generically on the declared event (no action-specific event names in the plugin). See change: finalize-event-dispatched-automation-runs. |
| `automation-schema.ts` | `parseAutomationYaml(text, knownKinds)`. Validates on/action/model/mode/sandbox/concurrency/visibility. Applies defaults mode=worktree, sandbox=workspace-write, concurrency=skip. Unknown trigger kind → error. Accepts taxonomy category kinds + on.events[]; requires non-empty events for multi-type categories; parses optional disabled boolean. See change: add-automation-plugin. See change: redesign-automation-editor-and-board. `parseAutomationYaml(text,knownKinds,knownActionIds=∅)`. validateAction accepts any registered `<source>.<verb>` id, keeps prompt/skill, parses optional `action.payload` map, isolates unknown action id. See change: register-plugin-automation-events. Adds `actions:[]` (mutually exclusive with `action:`), optional per-entry `count` (int≥1, honored on the single `action:` too), and `maxConcurrentSpawns` (int≥1); `config.action` now optional; validateAction parses `count`; both-forms declared and empty `actions:[]` fail; an unregistered entry error names `actions[i]`. See change: add-automation-concurrent-spawn. |
| `automation-watcher.ts` | `createAutomationWatcher`. fs.watch `<scopeBase>/.pi/automation/` recursive, 300ms debounce, filter `<name>/automation.yaml\|prompt.md`. Cloned from openspec-change-watcher. See change: add-automation-plugin. Adds `attachedBases()` + exported `reconcileWatchers(watcher, wantBases)` — detach not-wanted / attach newly-wanted, no-op in steady state (replaces detachAll+re-attach-all churn on ~300 recursive FSEvents handles). See change: fix-automation-watcher-rearm-churn. |
| `automation-writer.ts` | `writeAutomation` writes automation.yaml (+prompt.md for prompt action). Gains intent "create"\|"update"; create rejects existing-name collision; update requires existing + overwrites. `isValidAutomationName` rejects traversal + `runs`. `deleteAutomation`. See change: add-automation-plugin. See change: redesign-automation-editor-and-board. Serializes the `actions:` fan-out form + per-entry `count`; single `action:` prompt normalization guards the now-optional `config.action`. See change: add-automation-concurrent-spawn. |
| `cron.ts` | Thin re-export of ../shared/cron.js (parseCron, nextFire, isValidCron). See change: add-automation-plugin. See change: redesign-automation-editor-and-board. |
| `engine.ts` | Wires registry+scheduler+runner+scanner+watcher+run-store+model-resolver. `startRunFor` resolves model, writes running record, spawns via ctx.spawnSession stamped automationRun+visibility. `onSessionRegistered`/`onSessionEnded` deliver prompt + capture result.md. `buildRunPrompt`, `effectiveVisibility`. Adds `pendingForRunId(runId)` + `onSessionRegisteredForRun(sessionId, runId)` for exact runId correlation, immune to same-cwd FIFO races; `pendingForCwd`/`onSessionRegistered` retained. SpawnLike forwards mode + sandbox from config to spawn hook. EngineDeps gains optional abortSession(sessionId). Adds stopRun(runId): aborts run session via abortSession, finalizes record once (status error, result marker "_(stopped by user)_", error "stopped by user"), removePending so later onSessionEnded is no-op — idempotent vs agent_end. Returns false when run unknown/finalized. See change: add-automation-plugin. See change: fix-automation-run-correlation. See change: redesign-automation-editor-and-board. See change: automation-ui-mockup-parity. `buildRunPrompt(automation,actionRegistry?)` delegates to registered action buildPrompt (normalized kind), legacy prompt/skill fallback. EngineDeps.actionRegistry optional. Engine exposes actionRegistry. scanAutomations passes actionRegistry.ids(). See change: register-plugin-automation-events. Adds `buildRunDispatch` returning prompt-or-event union; RunContext carries optional `emitEvent`. Finalization unchanged (agent_end). See change: automation-emit-configured-event. EngineDeps gains `resolveRegistry: () => ActionRegistry` (replaces held `actionRegistry`). Collects fresh registry at dispatch + scan. See change: decouple-automation-action-registry. RunContext gains `spawnToken` (captured from spawn result in startRunFor's .then) — process handle before sessionId binds. EngineDeps replaces `abortSession` with `abortSpawnedRun({sessionId?,spawnToken?,graceful?}):Promise<boolean>`. `stopRun` now async (Promise<boolean>): hard-kills via abortSpawnedRun (sessionId or pre-register spawnToken) BEFORE finalizing. `onSessionEnded` terminates the persistent `--mode rpc` session graceful:true after capture (runs after removePending — idempotent). See change: fix-automation-stop-zombie-runs. RunDispatch{event}/RunContext.emitEvent carry optional `completion`; buildRunDispatch propagates it from the action's buildEvent. index.ts onEvent records `completion` per session at delivery and finalizes an event-dispatched run on its declared completion event (event runs emit no agent_end); prompt/no-completion runs still finalize on agent_end; idempotent. See change: finalize-event-dispatched-automation-runs. Adds `onSessionDeath(sessionId, result?)` — finalize a tracked run whose session died before a terminal event (buffered result→done, else error "session ended before completion") + free slot, idempotent (no-op if already finalized); `reapStaleRuns()` sweep + 60s timer started in `start()`/cleared in `dispose()`; `EngineConfig.maxRunAgeMs`; `findByRunId` helper (stopRun reuses). See change: finalize-automation-run-on-session-death. EngineDeps.abortSpawnedRun renamed from abortAutomationRun (shared primitive, generic). See change: add-goal-session-supervisor. Reaper warn line renamed to `[finalize] path=reaper` (a completed run finalized by the reaper = delivery defect, not a normal terminal state). See change: fix-automation-run-lifecycle. FAN-OUT: `startRunFor` resolves the model once → `resolveChildren(automation, effectiveBound)` → writes a PARENT occurrence record → spawns one child session per resolved child stamped `automationRun.runId=childRunId` (per-child automation view so `buildRunDispatch` reads that child's action). Per-parent `ParentState` counter finalizes the parent exactly once when the last child terminates (aggregate: error if any errored, else stopped if all stopped, else done; findings summed) and owns the single `runner.completeRun(key)` call (child finalization does NOT release the slot). `finalizeChild`/`finalizeParent`/`stopChild` replace `finishAndRelease`; RunContext gains `parentRunId`/`actionLabel`/`stopRequested`/`finalized`. `stopRun(runId)` resolves a parent (cascades to every live child) OR a child (single); spawn-window guard aborts on token arrival via `stopRequested`. Reaper enumerates child records (via `listStaleRunningRuns`) and never orphan-finalizes a parent with live children. `RunStatus` gains `stopped`; child `sessionId` persisted via `setSessionId`. `EngineConfig.maxConcurrentSpawns`. See change: add-automation-concurrent-spawn. |
| `index.ts` | registerPlugin. Mounts REST routes synchronously; defers engine init via queueMicrotask (avoids blocking boot on yaml/engine import). Wires runNow route hook to engine.startRunFor; engineRef holder; runNowViaEngine scans scope, fires one run. onEvent correlates run session strictly by host-applied `automationRun.runId` stamp (NOT cwd-FIFO); cwd match removed to stop delivering prompt to unrelated same-cwd sessions. Capture anchors on `turn_end` event (live-verified), NOT `message_end`; run session emits assistant message_start -> message_update* -> turn_end -> agent_end, NO assistant message_end, only user messages emit message_end; turn_end carries finalized assistant message; requires explicit `role==="assistant"`; concatenates `{type:"text"}` content blocks (drops thinking blocks), also accepts string content; excludes injected action prompt (delivered as `input` event + user message) via turn_end anchor + role guard + identity check vs run `promptText`; flushes result.md on agent_end. Passes abortSession:(id)=>ctx.abortSession(id) into createEngine. Mounts stopRun route hook → stopRunViaEngine(runId) → engineRef.stopRun. See change: add-automation-plugin. See change: fix-automation-run-correlation. See change: fix-automation-result-capture. See change: redesign-automation-editor-and-board. See change: automation-ui-mockup-parity. Creates+provides `automation.action-registry` synchronously in registerPlugin; passes registry to engine; mounts listActions hook. ACTION_REGISTRY_SERVICE const. See change: register-plugin-automation-events. On run-session register, event actions dispatch via `emitEventToSession`; prompt actions via sendToSession. See change: automation-emit-configured-event. Drops shared registry provide. Publishes core.* under `automation.action.core`. Collects contributions via `ctx.consumeAll("automation.action.")` on read. Passes `resolveRegistry` thunk to engine. See change: decouple-automation-action-registry. Subscribes `ctx.onSessionEnded` → `engine.onSessionDeath(sessionId, buffered)` (flushes runText/runPrompt/runCompletion first). `AutomationPluginConfig.maxRunAgeMs` (default 30min). See change: finalize-automation-run-on-session-death. Registers a `plugin_action` handler: `run`→`runNowViaEngine`, `stop`→`stopRunViaEngine`, `create`→`writeAutomation` (validated via `isValidAutomationName` + `unknownActionKind(config, collectRegistry().ids())`); dispatches the SAME engine cores the routes call (no HTTP re-entry); guarded on required fields. See change: fix-plugin-action-fanout-and-handlers. `attachWatchers()` delegates to `reconcileWatchers`; rescan debounce `RESCAN_DEBOUNCE_MS = 15_000` (was inline 2000). See change: fix-automation-watcher-rearm-churn. Logs the finalize path taken: `[finalize] path=completion-event` / `agent_end` / `session-death` (engine logs `path=reaper`), so a systematic forwarding outage cannot masquerade as many independent max-age timeouts. See change: fix-automation-run-lifecycle. `AutomationPluginConfig.maxConcurrentSpawns` (default 4) threaded into `EngineConfig`; the `create` plugin_action `unknownActionKind` guard now validates every `actions:` entry. See change: add-automation-concurrent-spawn. |
| `model-resolver.ts` | `resolveModel(model, {readRoles, defaultModel})`. `@role` → providers.json#roles; bare id passthrough; unresolved → defaultModel + error. `readRolesFromDisk`. See change: add-automation-plugin. |
| `resolve-children.ts` | Pure fan-out resolution. `resolveChildren(automation, bound) → {specs: ChildSpec[]; truncated}` expands `action:` \| `actions:[]` × per-entry `count` in declaration order and truncates at `bound` (keeps the first N; deterministic). `effectiveBound(automation, settingsDefault)` = per-automation `maxConcurrentSpawns` ?? default. `actionLabelFor(action)`, `DEFAULT_MAX_CONCURRENT_SPAWNS=4`. No spawn I/O — the bound warning is data. See change: add-automation-concurrent-spawn. |
| `routes.ts` | Mounts `/api/plugins/automation/{list,runs,result,create}` + DELETE. Adds GET /trigger-kinds (taxonomy descriptors), POST /update, GET /definition (config+promptBody), GET /git-capable (git work-tree probe via platform/exec execFileSync), POST /run (manual single run via runNow hook). Handlers lazy-import heavy modules. Registered before fastify.listen. Adds POST /api/plugins/automation/stop (scope+cwd+runId) → stopRun hook; 400 missing runId, 503 no engine, 400 on failed stop. AutomationRouteHooks gains stopRun. See change: add-automation-plugin. See change: redesign-automation-editor-and-board. See change: automation-ui-mockup-parity. Adds `GET /api/plugins/automation/actions?cwd=` serving descriptorsForCwd via `listActions` hook. See change: register-plugin-automation-events. Exports `unknownActionKind(config, ids)` for reuse by the plugin_action `create` handler. See change: fix-plugin-action-fanout-and-handlers. `unknownActionKind` validates every `actions:` entry (names `actions[i]`); `/runs` attaches `childRuns` to parent records; `/result` resolves a parent OR child run id via `resolveRunDir`; `/definition` guards the now-optional `config.action`. See change: add-automation-concurrent-spawn. |
| `run-store.ts` | Run records under `runs/<runId>/{result.md,run.json}`. `startRun`, `finishRun` (auto-archive empty, prune keep-N), `listRuns`, `pruneRuns`, `makeRunId`. DEFAULT_RETENTION=100. finishRun computes findings via countFindings(result) — count of top-level markdown bullet lines (/^[-*] +\S/), 0 when archived/empty; persists findings on RunRecord. Exports countFindings. See change: add-automation-plugin. See change: automation-ui-mockup-parity. Adds `listStaleRunningRuns(scopeBase, maxAgeMs, now?)` — `running` records past age, reaper input. FAN-OUT parent/child layout: `resolveRunDir(scopeBase, runId)` resolves a top-level (parent/flat) OR one-level-nested child dir (every consumer routes through it); `startParentRun` (writes `children:[]`), `startChildRun` (nested under parent + appends to `children`), `finishParentRun` (direct write, no result.md/auto-archive, summed findings + optional `warning`), `readChildRuns`, `setSessionId`. `listStaleRunningRuns` enumerates child + legacy-flat running (never parents); `pruneRuns` counts top-level only and SKIPS still-`running` occurrences (live-occurrence guard). See change: add-automation-concurrent-spawn. |
| `runner.ts` | Concurrency state machine. `fire` applies skip (drop) \| queue (defer) \| parallel. `completeRun` drains queue. Delegates start to injected startRun. See change: add-automation-plugin. |
| `scanner.ts` | `scanAutomations({repoRoot,homeDir,scanFolder,scanGlobal}, knownKinds)`. Dual-scope scan `.pi/automation/<name>/automation.yaml`. Scope tagging. Invalid isolated. Ignores `runs/`. See change: add-automation-plugin. scanScope/scanAutomations thread `knownActionIds` into parseAutomationYaml. See change: register-plugin-automation-events. |
| `schedule-trigger.ts` | `schedule` TriggerType. `parse` validates cron; `arm` self-reschedules per nextFire. Restart catch-up skip (next fire forward). See change: add-automation-plugin. |
| `scheduler.ts` | `createScheduler`. `armAll`/`rearmOne` dispose+arm valid automations via registry. `armOne` skips automations with config.disabled (dormant until re-enabled). `onFire`→runner.fire. `automationKey` = scope:name. Isolates invalid. See change: add-automation-plugin. See change: redesign-automation-editor-and-board. |
| `trigger-registry.ts` | `TriggerType` interface (kind/parse/arm→Disposable) + `TriggerRegistry`. Extensibility seam for future event/plugin kinds. Adds TRIGGER_TAXONOMY static category×event roadmap, deriveTriggerTaxonomy(registry) → descriptors (enabled iff kind registered, events forced planned under planned category), onKindForCategory/categoryForOnKind (scheduled↔schedule mapping). See change: add-automation-plugin. See change: redesign-automation-editor-and-board. |