AGENTS.md@packages/automation-plugin/src/server · git:20260901.30f41df · 2026-09-01 · sha256 cb297149aa17864e
AGENTS.md@packages/automation-plugin/src/server git:20260901.30f41dfA
Immutable. This exact content is served forever at /api/v1/blob/cb297149aa17864e.
# 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. Adds 4th param `knownSourceIds` (registered work-source ids); a `schedule.batch` kind requires a registered `on.source` (unknown → isolate), requires a single `action:` (rejects `actions:` and a per-action `count`). See change: automation-work-source-fanout. |
| `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. WORK-SOURCE FAN-OUT: registers `scheduleBatchTrigger`; `EngineDeps.workSources?: WorkSourceRegistry` (stable, lease-stateful — one instance for engine life) exposed as `engine.workSources`. `startRunFor` branches on `on.kind==="schedule.batch"` → `startWorkSourceFire`: leases `source.next(bound)` AFTER the runner admits the fire, spawns one child per handle with a per-child `FireContext{value:item}` (per-child `${{trigger}}`) + injected `idempotencyKey` (on the spawn stamp). Empty vend → completed no-op parent (returns null, holds no slot); `next` throws → errored parent, nothing leased; excess items deferred unleased (no truncation warning). `RunContext` gains `lease{source,token}` + `idempotencyKey`; `finalizeChild` releases the lease on every terminal path (done→ack, error/stopped/death→nack; stale/expired token = source no-op). `spawnChild` releases the lease + settles the parent on a SYNCHRONOUS setup failure (deferred to a microtask so `runner.begin` records the parent active before `completeRun`, freeing the slot); dispatch is built BEFORE the child record so a dispatch throw leaves no orphan; `stopChild` finalizes even if the abort rejects. Every PRE-spawn error path (source unregistered, `next()` throw, unresolved single-action spec) nacks each leased handle before settling the parent errored (no strand until visibility timeout). See change: automation-work-source-fanout. |
| `folder-work-source.ts` | `createFolderWorkSource({dir, visibilityTimeoutMs?, now?})` → `WorkSource<string>`. Files under `dir` are available items; `next(n)` reclaims EXPIRED + ORPHANED leases (scans `inflight/` on disk → restart recovery) then leases up to `n` by renaming each file into `inflight/<token>/` (rename = the fence); item = in-flight path, `idempotencyKey` = sha256(name+size+mtime).slice(0,16) (stable across redelivery, distinct for a new file reusing a name). `ack` deletes the in-flight file, `nack` returns it to `dir`; both no-op on a stale/EXPIRED token (expiry returns the item). Return-to-pool never deletes a file whose name already reappeared (no data loss). Rejects non-positive `visibilityTimeoutMs`. ONE live source per `dir` (in-memory leases). Clock injected for tests. See change: automation-work-source-fanout. |
| `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. Builds a stable `WorkSourceRegistry` from `AutomationPluginConfig.workSources[]` (folder-backed `createFolderWorkSource` per `{id,dir,visibilityTimeoutMs?}`) passed to the engine; `maxConcurrentSpawns` default resolved via `settingsDefaultBound(config, PI_AUTOMATION_MAX_CONCURRENT_SPAWNS env)`; `runNowViaEngine` threads `eng.workSources.ids()` into `scanAutomations`. The registration loop validates each entry (non-empty id/dir, positive timeout, unique resolved dir) and `ctx.logger.warn`s + skips a rejected one; `createFolderWorkSource` construction (fs I/O) is wrapped in try/catch so one bad dir can't abort engine init. See change: automation-work-source-fanout. |
| `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. Adds `settingsDefaultBound(configValue, envValue)` — settings-default precedence dashboard-config → `PI_AUTOMATION_MAX_CONCURRENT_SPAWNS` env → 4 (per-automation still wins via `effectiveBound`). See change: automation-work-source-fanout. |
| `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. Adds trailing `knownSourceIds` param threaded into parseAutomationYaml (validates `on.source`). See change: automation-work-source-fanout. |
| `schedule-batch-trigger.ts` | `scheduleBatchTrigger` (`kind: schedule.batch`) TriggerType. `parse` validates `cron` + a non-empty `on.source`; `arm` reuses the `schedule` cron timer (restart catch-up skip). The trigger vends nothing on fire — the engine leases items from `on.source` and fans out one child per handle. See change: automation-work-source-fanout. |
| `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. Shared `setTimer` seam wraps `setLongTimer(raw,now,fn,ms)` (exported; `MAX_DELAY`=2^31−1) so every trigger honors delays past Node's 32-bit setTimeout ceiling via recompute-to-target chunked hops. See change: add-automation-plugin. See change: redesign-automation-editor-and-board. See change: fix-schedule-timer-overflow. |
| `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. |
| `work-source-registry.ts` | `WorkSourceRegistry` — register/get/has/ids keyed by source id, mirroring `trigger-registry.ts`. Holds STABLE lease-stateful source instances. See change: automation-work-source-fanout. |