feat(flows): forge + agent nodes with rate-limit budget (NF-3) #349

Merged
code-lead merged 1 commit from feat/324-nf3-forge-agent-nodes into main 2026-04-24 12:41:32 +00:00
Collaborator

Summary

Extends the Node Flows registry (NF-2) with the full forge + agent node
catalog so flow authors can reach for any ForgePort method or an
agent dispatch from graph JSON.

  • 21 forge.* nodes — one per ForgePort method. Generated from a
    single declaration table in forge-nodes.ts. Each handler resolves
    its adapter through createForgeAdapterForRepo(repo, args.token) or
    an injected args.adapter (test path), then maps spec-shaped input
    ports onto the port method signature.
  • 3 agent.* nodesagent.dispatch, agent.cancel,
    agent.raise_cap. All three take an optional injection (args.dispatch
    / args.cancel / args.raiseCap) so the nodes stay testable without
    standing up a worker registry. Production wiring lands in NF-4.
  • Per-run rate-limit budget (rate-limit.ts) — default 20 mutating
    calls. RateLimitExceeded surfaces as a node-level throw, which the
    NF-2 executor treats as any other handler error (aborts unless the
    node sits behind router.try). Read-only forge queries do not charge
    the budget.
  • defaultRegistry() in registry.ts now calls registerForgeNodes +
    registerAgentNodes so the shipped registry already exposes every
    new type.

Design notes

  • Declaration table — 21 forge descriptors are generated from a
    ForgeNodeSpec list so each node entry is 5-15 lines rather than a
    full NodeDescriptor stanza. Adding a new forge method is one table
    row.
  • Port types use "any" — the spec called this out and NF-2's
    compiler treats any permissively. Domain shape validation stays on
    the ForgePort side (TypeScript) rather than forcing the registry to
    catalogue every ForgeIssue / ForgePullRequest field.
  • Repo input accepts both forms"owner/name" string and
    { owner, name } object. Saves author-facing transformers between
    upstream trigger payloads and the forge nodes.
  • Rate-limit injection is optionalresolveRateLimiter(args)
    first checks args.rateLimit, falls back to the module-level default.
    NF-4 will replace the default with a per-run instance built by the
    executor front-end. Until then, resetDefaultRateLimiter() keeps
    tests hermetic.
  • agent.dispatch completion wiring is a documented TODO — the
    node accepts an args.onTaskResult(taskId, cb) injection that
    NF-4 can route to the worker's onFinish hook. The NF-3 commit
    stops at returning the taskId and invoking the hook with a
    no-op — the full task.completed / task.failed trigger re-entry
    belongs to the executor plane, not this additive node layer.

Test plan

  • 86 new tests, all green — forge-nodes (42), agent-nodes (26),
    rate-limit (18). Cover: input mapping, output shape, error
    propagation, rate-limit budget (consume path, exhaustion,
    shared across nodes), adapter injection fallback, missing-token
    guard, spec input aliases.
  • bun x turbo run typecheck clean.
  • bun x biome check apps/server/src/ clean.
  • Existing registry.test.ts + executor.test.ts remain green
    after the defaultRegistry() extension.

Out of scope

  • Production wiring of args.dispatch / args.cancel / args.raiseCap
    from main.ts — NF-4.
  • Per-run rate-limit instance in the executor front-end — NF-4.
  • Task-completion trigger re-entry (task.completed / task.failed) —
    NF-4/NF-5.
  • UI surfacing of rate-limit state on the flow-run envelope — NF-UI.

Closes #324.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

## Summary Extends the Node Flows registry (NF-2) with the full forge + agent node catalog so flow authors can reach for any `ForgePort` method or an agent dispatch from graph JSON. - **21 `forge.*` nodes** — one per `ForgePort` method. Generated from a single declaration table in `forge-nodes.ts`. Each handler resolves its adapter through `createForgeAdapterForRepo(repo, args.token)` or an injected `args.adapter` (test path), then maps spec-shaped input ports onto the port method signature. - **3 `agent.*` nodes** — `agent.dispatch`, `agent.cancel`, `agent.raise_cap`. All three take an optional injection (`args.dispatch` / `args.cancel` / `args.raiseCap`) so the nodes stay testable without standing up a worker registry. Production wiring lands in NF-4. - **Per-run rate-limit budget** (`rate-limit.ts`) — default 20 mutating calls. `RateLimitExceeded` surfaces as a node-level throw, which the NF-2 executor treats as any other handler error (aborts unless the node sits behind `router.try`). Read-only forge queries do not charge the budget. - `defaultRegistry()` in `registry.ts` now calls `registerForgeNodes` + `registerAgentNodes` so the shipped registry already exposes every new type. ## Design notes - **Declaration table** — 21 forge descriptors are generated from a `ForgeNodeSpec` list so each node entry is 5-15 lines rather than a full `NodeDescriptor` stanza. Adding a new forge method is one table row. - **Port types use `"any"`** — the spec called this out and NF-2's compiler treats `any` permissively. Domain shape validation stays on the `ForgePort` side (TypeScript) rather than forcing the registry to catalogue every `ForgeIssue` / `ForgePullRequest` field. - **Repo input accepts both forms** — `"owner/name"` string and `{ owner, name }` object. Saves author-facing transformers between upstream trigger payloads and the forge nodes. - **Rate-limit injection is optional** — `resolveRateLimiter(args)` first checks `args.rateLimit`, falls back to the module-level default. NF-4 will replace the default with a per-run instance built by the executor front-end. Until then, `resetDefaultRateLimiter()` keeps tests hermetic. - **`agent.dispatch` completion wiring is a documented TODO** — the node accepts an `args.onTaskResult(taskId, cb)` injection that NF-4 can route to the worker's `onFinish` hook. The NF-3 commit stops at returning the `taskId` and invoking the hook with a no-op — the full task.completed / task.failed trigger re-entry belongs to the executor plane, not this additive node layer. ## Test plan - [x] 86 new tests, all green — forge-nodes (42), agent-nodes (26), rate-limit (18). Cover: input mapping, output shape, error propagation, rate-limit budget (consume path, exhaustion, shared across nodes), adapter injection fallback, missing-token guard, spec input aliases. - [x] `bun x turbo run typecheck` clean. - [x] `bun x biome check apps/server/src/` clean. - [x] Existing `registry.test.ts` + `executor.test.ts` remain green after the `defaultRegistry()` extension. ## Out of scope - Production wiring of `args.dispatch` / `args.cancel` / `args.raiseCap` from `main.ts` — NF-4. - Per-run rate-limit instance in the executor front-end — NF-4. - Task-completion trigger re-entry (`task.completed` / `task.failed`) — NF-4/NF-5. - UI surfacing of rate-limit state on the flow-run envelope — NF-UI. Closes #324. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(flows): forge + agent nodes with rate-limit budget (NF-3)
All checks were successful
qa / qa (pull_request) Successful in 4m32s
qa / dockerfile (pull_request) Successful in 9s
902089b13c
Closes #324.

Extends the Node Flows registry with 21 `forge.*` node types (one per
ForgePort method) and 3 `agent.*` nodes (dispatch / cancel / raise_cap).

- `forge-nodes.ts` drives 21 descriptors off a declaration table to keep
  boilerplate down. Each handler resolves the adapter from either
  `args.adapter` (test injection) or `createForgeAdapterForRepo(repo,
  args.token)`. Mutating calls charge one unit against a per-run
  rate-limit budget (default 20); read-only queries do not.
- `agent-nodes.ts` ships three nodes. `agent.dispatch` takes an
  injectable `dispatch` hook (wired to `dispatchByType` in NF-4) and
  returns the enqueued task id. `agent.cancel` + `agent.raise_cap`
  take matching injection hooks. All three consume one budget unit.
- `rate-limit.ts` ships a minimal `RateLimiter` helper with a
  module-level default instance plus `resolveRateLimiter(args)` so
  nodes share the same counter across a flow run.
- `registry.ts::defaultRegistry()` now calls `registerForgeNodes` +
  `registerAgentNodes` so the shipped registry already has every
  node type.

Tests: 86 new (forge-nodes 42, agent-nodes 26, rate-limit 18). Full
`bun x turbo run typecheck` + `bun x biome check` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
code-lead deleted branch feat/324-nf3-forge-agent-nodes 2026-04-24 12:41:34 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!349
No description provided.