Skip to content

ADR-0017: Action results are scoped to a single project

  • Status: proposed
  • Date: 2026-04-10
  • Deciders: @Aksem
  • Tags: actions, architecture, results, wm-server

Context

Every action defines a RunActionResult subclass with an update() method that the framework calls to merge incremental results during a run — streaming partial results from a handler, or consuming delegated partial results from a sub-action. The chosen merge semantics shape how result fields should be typed: flat list versus dict, scalar versus collection, flat versus nested.

When designing a new action result, a natural question arises: if this action ever runs across multiple projects as part of a workspace-level request, should the returned result represent all those projects in one merged value? If the answer is yes, every project-local field needs qualification — for example, installed_hooks: list[str] cannot stay flat because hook types are only unique within a single repository, so three projects each installing "pre-commit" would collapse into a meaningless list of duplicates. The same reasoning would cascade through most result types and force nested per-project structures everywhere.

The alternative is to treat cross-project aggregation as something that happens outside the result type. The WM already returns multi-project results as a dict keyed by project path — the runBatch API preserves each project's result independently and only combines return codes via a trivial bitwise OR. No caller currently relies on the WM producing a single merged result shape across projects: the CLI renders per project, MCP returns per-project JSON, and programmatic callers that need a cross-project view (for example an LSP server combining diagnostics) already maintain their own per-project state and perform the join in their own domain terms.

FineCode therefore needs an architectural rule about where cross-project aggregation lives, so that result-type authors have a clear scope for update() semantics and do not encode cross-project concerns into per-project result shapes.

  • ADR-0011 — progress streams must be aggregated at the WM because multiple concurrent lifecycles cannot share one client token. This ADR is the result-type counterpart and reaches the opposite conclusion: results need no aggregation because the WM returns them per project.
  • ADR-0012 — addresses aggregation granularity within one project (per item versus per batch). This ADR addresses the orthogonal question of aggregation across projects.
  • ADR-0009 — partial results flow through explicit handler mediation. That mediation stays within a single project's action call; this ADR clarifies that the accumulated result never needs to cross project boundaries inside the framework.
  • ADR-0016 — defines the invocation scopes (handler, extension runner, workspace). This ADR pairs with it by stating that result merging is scoped to a single project's action execution regardless of which invocation layer runs it.

Decision

Action results are scoped to a single project. The framework does not merge results across projects.

  • RunActionResult.update() is defined for intra-project merging only: partial-result streaming inside a single action call, and delegated sub-action results consumed by a parent handler within the same project.
  • Result field shapes are designed assuming the merge scope is one project. Within that scope, identifying keys such as file URIs, hook types, environment names, or handler names are already unique, and flat lists or dicts merge cleanly via extend and update.
  • Cross-project aggregation is a caller concern. Clients consume the WM's per-project return structure — currently the runBatch dict keyed by project path — and perform any cross-project join in their own domain terms.
  • Result types must not contain fields whose purpose is to represent a cross-project view. A skipped_projects: list[str] field inside a project-level result, for example, is a sign that cross-project concerns have leaked into the wrong layer; the equivalent information already lives in the per-project return structure.
  • Project-wide scalar outcomes such as "was this project skipped, and why" are appropriate as scalar fields on the result. Within a single project's scope, the value is scalar by definition and no list or dict wrapping is needed. When update() merges a scalar outcome from a later partial result, it adopts the later non-empty value.
  • An action whose primary job is to produce workspace-wide data is not excluded by this rule. Such an action is declared on a workspace-level project and returns a single result for a single action call; the rule governs how the framework handles multi-project orchestration of any given action, not what data an individual action is allowed to return.

Consequences

  • Result types stay simple. Fields can use the most natural shapes — flat lists, scalar outcomes — without defensive per-project qualification.
  • Cross-project views live where the domain knowledge lives. A caller that needs a workspace-wide rollup writes a trivial loop over the per-project structure, free to apply semantics the framework cannot know about.
  • No mode flag on the WM API. The WM does not distinguish "merged" from "unmerged" workspace returns; only the per-project shape exists. This keeps the API and its clients smaller.
  • New actions inherit a clear design rule. A result-type author only needs to reason about intra-project merging, not about a hypothetical cross-project merged form.
  • If a future caller genuinely needs native cross-project merging, this ADR must be revisited. The expected path forward would be a caller-side aggregation helper rather than a new WM mode, since the per-project structure already contains all the data needed.

Alternatives Considered

Declare merge-ability on each result type and support a merged mode in the WM API. Rejected because no current caller needs it, because it forces every result type to solve the cross-project aggregation problem at design time, and because the per-project return structure already contains the full information a caller needs to build its own merged view with the correct domain semantics.

Flatten and extend per-project results into a single workspace result by default. Rejected because project-local identifiers collide under flat extension and lose their project scope, producing results that are at best ambiguous and at worst actively wrong.

Leave the rule implicit. Rejected because the absence of a written rule produced inconsistent result-shape choices and surfaced the same design question repeatedly during new-action work.