Skip to content

feat: P4 Phase C — Build a flow (decomposition over the untouched pipeline) - #80

Merged
ryandmonk merged 2 commits into
mainfrom
feat/p4-flows-c
Aug 12, 2026
Merged

feat: P4 Phase C — Build a flow (decomposition over the untouched pipeline)#80
ryandmonk merged 2 commits into
mainfrom
feat/p4-flows-c

Conversation

@ryandmonk

Copy link
Copy Markdown
Contributor

Flow decomposition per the ratified P4 architecture: one workflow goal → an editable plan of 2–8 steps (composer-core flow-plan.ts on the plan.ts idioms — existing intents only, enum-clamped, honest labeled deterministic fallback that never blocks) → the flow created immediately with every step PENDING (first-class state: warn-level lint, disabled navigator chips, outline preview) → each step driven as an ordinary runBuild turn, strictly sequential with provider-aware spacing (the P3a burst finding), Accept pre-targeted to its step through the Phase B binding, per-step resume buttons.

The generation pipeline, contracts, emit, A2UI, renderers, intents, and governance are untouched; no test makes a live gateway call; with the flow-mode toggle untouched the single-surface Build path renders byte-identically (pinned by e2e). runBuild now resolves the turn's outcome (computed from the turn's own events) so the driver can sequence — void callers unaffected.

Fail-first: pre-change failures pasted in 396ed22. composer-core 77→89, composer 65→68, smoke e2e 12→13, composer-agent 44/44 re-run, typecheck clean.

🤖 Generated with Claude Code

ryandmonk and others added 2 commits August 12, 2026 08:58
…teps, Build-a-flow e2e

New assertions, run against main (A+B merged, studio#79) — all fail for the
right reasons; NO live gateway call anywhere (the decomposition request is
tested shape-only, the plan.test.ts discipline):

- packages/composer-core/src/flow-plan.test.ts (new) — buildFlowPlanRequest
  (goal as the user message; system names the design system, lists intent
  ids, states the 2–8 ordered ONE-screen-per-step rules, forbids invented
  steps; schema: steps minItems 2 / maxItems 8, closed objects, intent as a
  REAL-id enum, optional per step), reconcileFlowPlan (valid plan kept
  source "model"; invalid/missing step intent -> planDeterministic on THAT
  step's goal; bounds 8 steps + name/title/goal slices; <2 usable steps or
  garbage -> deterministic fallback, never an empty plan), and
  flowPlanDeterministic (a Gateway-#12-shaped numbered lifecycle fixture ->
  >=4 ordered steps, real intents, labeled scripted with a deterministic
  reason; sentence clustering; single-sentence goals still outline >=2
  steps; rambling goals bounded to 6).
- apps/composer/app/flows.test.ts — surfaceId "" is a PENDING step: flowLint
  warns "not built yet" (code pending-step) instead of the dangling error,
  one cause one finding (no advanceOn warn on a pending step); parseFlow
  shape anchor ("" is a state, not malformation).
- packages/composer-core/src/flows.test.ts — flowSchema/parseFlows anchor:
  an empty surfaceId parses (already true; pinned so it stays true).
- e2e/composer-prod-smoke.spec.ts — one added spec: default Build render has
  NO flow-mode elements; toggle -> 3-sentence goal -> deterministic labeled
  plan -> edit (remove a step, retitle, pin intents) -> "Create flow & build
  steps" -> flow exists PENDING -> two sequential ordinary scripted builds ->
  accept turn 1 (pre-targeted by the driver) -> Preview shows the half-built
  flow with the pending step disabled + the pending body state -> accept
  turn 2 -> the completed flow walks end to end.

Pre-change output:

 composer-core (npx vitest run src/flow-plan.test.ts src/flows.test.ts):
 ✓ src/flows.test.ts (7 tests)            [pending-shape anchors hold]
 FAIL  src/flow-plan.test.ts [ src/flow-plan.test.ts ]
 Error: Cannot find module './flow-plan'

 apps/composer (npx vitest run app/flows.test.ts): 2 failed | 30 passed
   × an EMPTY surfaceId is a PENDING step (Phase C): warn 'not built yet', never a dangling error
     → expected 'error' to be 'warn'
   × a pending step never also warns about its advanceOn (one cause, one finding)
     → expected 'dangling-surface' to be 'pending-step'

 e2e smoke (-g "build a flow", against the freshly built static export of main):
   Error: locator.click: ... waiting for getByTestId('build-mode-flow')
   1 failed

Note: the Gateway #12 corpus text lives outside this repo; the fixture
mirrors its shape (one workflow goal, numbered stations) and is labeled so.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eps, sequential step driver

One workflow goal becomes an EDITABLE plan; every step then builds through
the ordinary governed pipeline — same gates, same repairs, same Accept —
one step at a time. dspack-gen never learns flows exist; the planner is
Composer/composer-core code beside plan.ts, and no test touches a live
gateway (the decomposition request is shape-only, the plan.test.ts rule).

- composer-core flow-plan.ts (new, plan.ts idioms): buildFlowPlanRequest —
  system names the design system, lists the contract's intents, states the
  2–8 ordered ONE-screen-per-step rules and forbids invented steps; schema
  closes every object, bounds steps 2–8, and constrains step intent to a
  REAL-id enum (optional; the reconciler fills gaps). reconcileFlowPlan —
  clamps each step's intent (invalid/missing -> planDeterministic on THAT
  step's goal), bounds name/title/goal with the same slice idiom, bounds to
  8 steps, and falls back to the deterministic outline when fewer than 2
  usable steps remain; source "model". flowPlanDeterministic — the honest,
  never-blocking outline: one step per numbered item (the workflow framing
  line is not a step), else per sentence clustered to at most 6, else the
  goal plus a review step; per-step intents through the same deterministic
  classifier scripted builds use; labeled scripted with a reason that says
  it is a deterministic outline.
- Pending steps are first-class: surfaceId "" means PLANNED, NOT YET BUILT.
  flowLint warns (pending-step, "not built yet") instead of the dangling
  error — one cause, one finding; parseFlow/flowSchema/manifest accept ""
  (anchored). Preview renders pending navigator chips visibly disabled
  ("· pending"), and a pending current step shows a quiet outline state
  (flow-step-pending) — a flow of only pending steps previews as its
  outline, never a crash.
- Build gains an OPT-IN flow mode (build-mode-flow; the default
  single-surface render and behavior are untouched — the e2e pins that no
  flow-mode elements exist until the toggle): goal textarea -> "Plan the
  flow" (hosted plans via the existing runGatewayRequest seam, any throw ->
  the labeled deterministic outline; scripted/local plan deterministically)
  -> an editable plan editor (title/goal/intent per step, add/remove/
  reorder, Phase A editor idioms) -> "Create flow & build steps": the flow
  is created IMMEDIATELY with every step pending through the single
  saveFlows funnel, then the driver runs each step as an ordinary runBuild
  turn (goal = step.goal, intentOverride = step.intent) STRICTLY
  sequentially — never two builds in flight — spaced 8s for model providers
  (the P3a burst finding; scripted has no provider to protect and runs
  back-to-back). Each result is a normal turn whose Accept is PRE-TARGETED
  to its step via the Phase B binding (the header select still wins); a
  failing step stops the drive, later steps stay pending, and per-step
  "build step" buttons resume or re-run individually.
- runBuild now RESOLVES with the turn's outcome, computed from the turn's
  own event stream (never from state) so the driver can sequence and stop;
  single-surface callers ignore it. Turns carry an optional flowStepHint;
  the accept note names the pre-targeted step.

All suites green: composer-core 77 -> 89, composer 65 -> 68, agent 48
(untouched), typecheck clean, smoke e2e 12 -> 13 (existing specs untouched),
composer-agent e2e 44/44 against the real agent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 12, 2026 13:15
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
dspack-studio-composer f06b296 Aug 12 2026, 01:17 PM

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds P4 Phase C “Build a flow” support by introducing flow decomposition planning (2–8 step editable plans) and a sequential per-step build driver, while keeping the underlying single-surface generation pipeline unchanged until the flow-mode toggle is enabled.

Changes:

  • Introduces flow-plan planning primitives in composer-core (schema, provider-agnostic request builder, deterministic fallback, reconciliation).
  • Adds an opt-in “Build a flow…” mode in Build UI with plan editing, immediate creation of PENDING flows, and sequential step builds with provider-aware spacing.
  • Updates flow parsing/linting/preview to treat surfaceId: "" as a first-class “PENDING” step state, plus adds/extends unit + e2e coverage.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/composer-core/src/index.ts Re-exports new flow-plan APIs/types.
packages/composer-core/src/flows.test.ts Adds a contract-level shape anchor test for empty surfaceId (pending step).
packages/composer-core/src/flow-plan.ts New flow decomposition planner: schema, request builder, deterministic fallback, reconciler.
packages/composer-core/src/flow-plan.test.ts New unit tests for flow-plan request shape, reconciliation, and deterministic behavior.
apps/composer/app/views/preview-view.tsx Disables pending-step chips and renders an outline/pending empty-state instead of treating it as an error.
apps/composer/app/views/build-view.tsx Adds flow-mode UI, plan editor, flow creation with pending steps, and sequential step build driver + accept targeting.
apps/composer/app/state.tsx Extends runBuild to return a turn outcome and tags turns with flowStepHint for accept pre-targeting.
apps/composer/app/planning.ts Adds planFlow() with hosted inference + deterministic fallback.
apps/composer/app/flows.ts Adds pending-step helpers and updates flow lint to warn (not error) on pending steps.
apps/composer/app/flows.test.ts Adds tests for pending-step lint behavior and parse acceptance.
e2e/composer-prod-smoke.spec.ts Adds a full “build a flow” smoke test covering planning/editing/pending preview/sequential builds.
Suppressed comments (3)

apps/composer/app/views/build-view.tsx:620

  • After a flow has been created (flowBuild !== null), the step title/goal/intent inputs remain editable. This can desync the persisted flow (already saved via saveFlows with step titles) from what the Build UI shows/uses for subsequent step builds, and it undermines the intended "edit → approve → build" lifecycle. Consider locking the step fields once the flow is created, not just while it's running.
                  <input
                    style={{ ...field, minWidth: 140 }}
                    value={step.title}
                    onChange={(e) => patchPlanStep(i, { title: e.target.value })}
                    disabled={flowBuild?.running === true}

apps/composer/app/views/build-view.tsx:699

  • The "Create flow & build steps" button is enabled for 1-step (or >8-step) plans, which contradicts the 2–8 step contract and can lead to out-of-contract flows. Disable this action unless the plan is within bounds.
                  <button
                    className="st-btn st-btn--primary"
                    disabled={flowPlan.steps.length === 0 || planBusy || buildBusy}
                    onClick={() => void acceptPlan()}
                    data-testid="flow-plan-accept"
                  >
                    Create flow &amp; build steps

apps/composer/app/views/build-view.tsx:485

  • acceptPlan() only guards against an empty plan, but it should enforce the same 2–8 step bounds as the flow-plan schema; otherwise a manually edited plan can create an out-of-contract flow and the driver will attempt to build an arbitrary number of steps.
  const acceptPlan = async () => {
    if (!flowPlan || flowPlan.steps.length === 0 || planBusy || buildBusy || flowBuild?.running) return;
    const plan = flowPlan; // frozen for this drive; the editor locks while running

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +456 to +459
const removePlanStep = (at: number) => setFlowPlan((p) => (p ? { ...p, steps: p.steps.filter((_, i) => i !== at) } : p));

const addPlanStep = () =>
setFlowPlan((p) => (p ? { ...p, steps: [...p.steps, { title: `Step ${p.steps.length + 1}`, goal: "", intent: intents[0] ?? "" }] } : p));
Comment on lines +599 to +603
<input
style={{ ...field, minWidth: 220 }}
value={flowPlan.name}
onChange={(e) => setFlowPlan((p) => (p ? { ...p, name: e.target.value } : p))}
disabled={flowBuild?.running === true}
@ryandmonk
ryandmonk merged commit 926e426 into main Aug 12, 2026
3 checks passed
@ryandmonk
ryandmonk deleted the feat/p4-flows-c branch August 12, 2026 13:23
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.

2 participants