Skip to content

docs(commands): command-system design proposal (commands as a core foundation)#250

Open
Stvad wants to merge 7 commits into
masterfrom
design/command-system
Open

docs(commands): command-system design proposal (commands as a core foundation)#250
Stvad wants to merge 7 commits into
masterfrom
design/command-system

Conversation

@Stvad

@Stvad Stvad commented Jun 23, 2026

Copy link
Copy Markdown
Owner

What

A high-level design proposal for upgrading the action system into a typed, observable command substrate — the highest-leverage extensibility primitive we identified after the navigation-seam work.

Design-only: one HTML doc, docs/command-system-design.html. No code changes.

Why

The navigation work proved a pattern we keep repeating: to make one behaviour extensible (observe / wrap / veto / replace), we hand-build a bespoke seam for it (navigationVerb, pasteDecisionVerb). Each is correct in isolation but is a new vocabulary the rest of the system doesn't share — the "double thing" smell.

The doc's organizing idea:

Command ≈ Action's identity / binding / context ∪ Verb's typed signature / middleware

We already have two partial command systems that don't know about each other:

  • the action system — identity, binding, context, gating; but no result channel and no invocation-time middleware
  • defineVerbFacet — typed signature + impl/decorator/before/after middleware; but no identity, no binding

The four gaps it frames

Gap Status
G1 Typed result channel only structural blocker; why navigate needed a bespoke verb
G2 Typed Args vs injected deps
G3 Invocation-time middleware = spawned S1 task
G4 One dispatch lifecycle collapses 3 divergent entries + the canDispatch split the code already flags as a TODO

Also covers: commands↔events (with the audit-B3 typed-bus constraint), why the bespoke verbs fold into the generic bus once G1+G3 land, a return-channel-first migration path, and open questions.

Grounding

Re-read against master: src/shortcuts/types.ts, src/shortcuts/runAction.ts, src/shortcuts/effectiveActions.ts, src/facets/verbFacet.ts, src/plugins/app-intents/appIntents.ts.

Scope

Proposal for discussion — not an implementation plan. Companion to docs/extensibility-axes.md, docs/extension-seam-gaps.md, docs/navigation-redesign.md.

🤖 Generated with Claude Code

Propose upgrading the action system into a typed, observable command
substrate (commands as a core foundation, events as the reactive twin).
Frames the four gaps that separate today's Action from a command —
result channel (the only structural blocker), typed args vs deps,
invocation-time middleware, one dispatch lifecycle — and the equation
Command = Action's identity/binding/context union Verb's typed
signature/middleware. Lays out the model, how commands relate to events,
why the bespoke verb seams (navigationVerb/pasteDecisionVerb) fold into
the generic bus, a return-channel-first migration path, and open
questions. Design-only; no code changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Stvad and others added 6 commits June 23, 2026 01:51
…verb nav, trust)

Revision pass after a 3-agent adversarial review. Findings addressed:

- BLOCKER: promote synchronous dispatch to a first-class requirement
  (new section 6). Async-only dispatch breaks preventDefault, the sync
  not-handled fall-through, and currentTarget capture on the keyboard/
  pointer/click hot path — the navigation work already had to carve out
  a sync escape around its async verb. Add dispatchSync + DECLINE.
- MAJOR: correct the critical path to G1 -> G3 -> G4 -> G2 -> fold.
  G1 alone only buys 'dispatch returns a typed result'; retiring the
  verb needs G3 (a home for its middleware) + G4 (uniform application).
- MAJOR: the false sentinel can't double as a typed Result; make the
  decline channel out-of-band (DECLINE).
- MAJOR: navigation is TWO verbs (intent pure/fallback + execute
  effectful/rethrow); one command can't carry two onError policies, so
  the fold yields a resolve/apply command pair.
- MAJOR: add trust/ordering/cost callout for untrusted-extension
  wildcard middleware; add an Alternatives-considered section (1.1) and
  drop the unsubstantiated 'single highest-leverage' superlative.
- MINOR: fix events 'after is in-path' contradiction; correct caller
  count (~20 -> 60+); fix srs example (wraps isVisible, not handler);
  reword 'no data return channel'; add failure-surfacing/test open Qs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Split Command into discriminated run (async, dispatch-only) vs runSync
  (Result | DECLINE, chord/pointer-bindable); state + type-enforce the
  invariant that sync-dispatched commands have synchronous run, mirroring
  how ActionHandlerResult forbids Promise<false> today.
- Wildcard middleware: async confirm-guards are async-path-only and would
  bypass keyboard-triggered commands; name the two-tier risk and require
  picking a protocol instead of advertising 'one guard for every command'.
- Navigate fold: scope honestly to 2 of 5 nav seams (back/forward +
  URL-restore bypass the verbs via writePanelContent); the click already
  goes through the bus today and apply is async, so re-scope the win to
  G1/G2/G3 rather than 'uniform middleware on the sync click'.
- Reconcile the (A)-vs-(C) stance: state recommendation (C) w/ (A)
  fallback; building past G1 commits to (C); decision owed before step 2.
- Minor: §3 priority-vs-build-order clause; add §1.1 to the ToC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ay be async

Self-caught overcorrection from the round-1 fix: I'd written runSync as
(i) => Result | DECLINE and 'chord-bound commands MUST be synchronous',
which would forbid async keyboard effects (delete -> repo.tx) that are
ubiquitous today. The real invariant mirrors ActionHandlerResult exactly:
it forbids Promise<false>, NOT Promise<void>. So runSync forbids
Promise<DECLINE> (the fall-through decision must be in-tick) but allows
Promise<Result> (the effect runs async, unawaited by the coordinator).
Signature -> typeof DECLINE | MaybePromise<Result>; §6 commitment reworded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…utor; arg-gating)

- S1: correct the paste claim. Paste is NOT gated on the sync-resolution
  task and is not a runSync command — its DOM handlers preventDefault
  eagerly and await the decision today, so it folds onto the async
  dispatch path (the easy case). The sync task gates keyboard-chord pure
  verbs, not the paste decision. (§6 footnote, §8 bullet)
- S2: the 'middleware = verb facet, generalized' lift is literal only for
  the async face. VerbFacet.run is async-only and awaits the impl to build
  VerbOutcome for after; the no-await dispatchSync can't, so the sync path
  needs its own lighter executor with a degraded after (sync before/
  decorator; invocation-only after). Stated explicitly. (§5)
- F1: note canDispatch/isVisible are deps-only (not args) by design — they
  run at chord-resolution before args exist; arg-dependent gating goes in
  run/runSync. (§5 sketch)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…substantive findings)

Two independent round-3 reviewers found the design coherent end-to-end
with no substantive findings; applying the trivial polish they flagged:
- G4 card: 'one lifecycle, two execution faces' instead of bald 'one
  pipeline' (the two-face reality is established in §5/§6/§9).
- §7: note the 'after = in-path lifecycle hook' framing is async-face
  only; on dispatchSync the outcome isn't awaited, so a sync-path after
  sees invocation, not success/failure (cross-ref §5 + the §10 open Q).
- §9: reword 'four dispatch entries' to 'dispatch-by-id call sites' to
  avoid clashing with §2's 'three entries' framing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(B+) staging, G4 gated

Adversarial re-review (design-soundness + ground-truth + quality lenses)
found the (C) recommendation mispriced at the decisive comparison and
three contract holes in the §5 sketch. Substantive changes:

- §1.1: re-price (B) — both verbs retire on the async face under
  G1+G2+G3 (navigate.apply is irreducibly async; keyboard navigations
  route through navigate() internally). New recommendation: staged
  path, G4 + sync-face middleware behind a named gate.
- §5: DECLINE representable on BOTH faces (no false→undefined-style
  coercion can reappear); run gains a DECLINE arm; typed-handles-first
  (by-string dispatch is the explicitly-untyped edge, not a fake
  generic — VS Code executeCommand<T> cited as the anti-pattern,
  in-repo CodeMirror StateCommand as corroborating prior art);
  veto needs its own VETOED outcome (sync DECLINE-veto would release
  the chord to the browser un-preventDefaulted); resolution named as
  lifecycle stage zero with the id-uniqueness invariant and the
  per-winner middleware granularity; args provenance for bound/palette
  commands stated.
- §2: table corrected (pointer/gesture take supplied deps, no
  activation requirement, return handled-boolean) + agent bridge added
  as the fifth, dispatch-bypassing path with an untyped result channel;
  transform inventory completed (srs handler wraps, spatial description
  replacement).
- §9: counts corrected (≈17 prod call sites, not 60+); critical path
  G1→G2→G3→folds with G4 gated; step 3 gains the coverage duty (hold
  fire / gesture progress / agent bridge bypass the shared loop) and
  the sync-deciding contract; step 4 fixes the paste contradiction and
  adds the two fold caveats (extension-barrel shim, effect-free intent
  probe); step 5 reframed as policy-matrix with the palette
  isVisible/canDispatch mismatch as the motivating defect.
- Quality pass: house doc-status banner added; review scar tissue
  excised; sync/async story consolidated into §6; warn callouts 7→2;
  decision promoted from footnote to a keyed callout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant