From 337bdcd0818e4ee08bae29c52c18ce582eb0484c Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 11 Jul 2026 00:06:08 -0700 Subject: [PATCH] feat(compound-engineering): align the UI with the upstream loop --- .changeset/clean-compound-pipeline.md | 4 +- .../compound-engineering/pipeline-desktop.png | 4 +- .../compound-engineering/pipeline-mobile.png | 4 +- .../src/__tests__/stage-registry.test.ts | 17 +- .../src/__tests__/sync.test.ts | 22 ++- .../src/artifacts/__tests__/discovery.test.ts | 2 +- .../src/artifacts/discovery.ts | 3 +- .../src/dashboard/CompoundEngineeringView.css | 23 ++- .../src/dashboard/CompoundEngineeringView.tsx | 170 +++++++++++++----- .../CompoundEngineeringView.test.tsx | 79 +++++++- .../src/session/stage-registry.ts | 19 +- 11 files changed, 265 insertions(+), 82 deletions(-) diff --git a/.changeset/clean-compound-pipeline.md b/.changeset/clean-compound-pipeline.md index 104f7734e9..c0f2de9fc5 100644 --- a/.changeset/clean-compound-pipeline.md +++ b/.changeset/clean-compound-pipeline.md @@ -2,6 +2,6 @@ "@runfusion/fusion": minor --- -summary: Support repeatable Compound Engineering cycles with project-scoped progress and artifact collections. +summary: Guide repeatable Compound Engineering cycles from product grounding through reusable learnings. category: feature -dev: Adds collection-aware stages, safer session controls, explicit choice confirmation, and terminal Work progression. +dev: Adds project-scoped sessions, collection-aware stages, Work quality gates, and terminal Compound progression. diff --git a/docs/screenshots/compound-engineering/pipeline-desktop.png b/docs/screenshots/compound-engineering/pipeline-desktop.png index 7791a07b2f..f13368fc77 100644 --- a/docs/screenshots/compound-engineering/pipeline-desktop.png +++ b/docs/screenshots/compound-engineering/pipeline-desktop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6c218824cbd21ec36305437e2adc5c810163ab25cd28fdee705bb12ddf48b93 -size 270173 +oid sha256:06d35e52a88966234de0aef50cc471a474a80422789cfaba5e3b8133f189e2d8 +size 251273 diff --git a/docs/screenshots/compound-engineering/pipeline-mobile.png b/docs/screenshots/compound-engineering/pipeline-mobile.png index 668a8fc751..1eaa719035 100644 --- a/docs/screenshots/compound-engineering/pipeline-mobile.png +++ b/docs/screenshots/compound-engineering/pipeline-mobile.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bbcbf6767d6982ebb4340498396dc1453eb2e7458c3f0877c20d7a4f5ece9d3 -size 95990 +oid sha256:5ddba7235bdf54679910eae60d9716ea631fec8ef5e0a8be651e197fca243225 +size 98300 diff --git a/plugins/fusion-plugin-compound-engineering/src/__tests__/stage-registry.test.ts b/plugins/fusion-plugin-compound-engineering/src/__tests__/stage-registry.test.ts index 6545ad778a..3ee1c1ddd5 100644 --- a/plugins/fusion-plugin-compound-engineering/src/__tests__/stage-registry.test.ts +++ b/plugins/fusion-plugin-compound-engineering/src/__tests__/stage-registry.test.ts @@ -8,20 +8,21 @@ describe("compound engineering stage registry", () => { const stageIds = listStages().map((stage) => stage.stageId); const pipelineStageIds = listPipelineStages().map((stage) => stage.stageId); - expect(stageIds.slice(0, 5)).toEqual(["strategy", "ideate", "brainstorm", "plan", "work"]); + expect(stageIds.slice(0, 6)).toEqual(["strategy", "ideate", "brainstorm", "plan", "work", "compound"]); expect(stageIds.at(-1)).toBe("debug"); expect(stageIds.filter((stageId) => stageId === "debug")).toHaveLength(1); expect(stageIds.indexOf("plan")).toBeLessThan(stageIds.indexOf("work")); - expect(stageIds.indexOf("work")).toBeLessThan(stageIds.indexOf("debug")); - expect(pipelineStageIds).toEqual(["strategy", "ideate", "brainstorm", "plan", "work"]); + expect(stageIds.indexOf("compound")).toBeLessThan(stageIds.indexOf("debug")); + expect(pipelineStageIds).toEqual(["brainstorm", "plan", "work", "compound"]); }); - it("advances through every automatic transition and treats work as terminal", () => { - expect(nextStageAfter("strategy")).toBe("ideate"); - expect(nextStageAfter("ideate")).toBe("brainstorm"); + it("advances through delivery into compounding and treats compound as terminal", () => { + expect(nextStageAfter("strategy")).toBeUndefined(); + expect(nextStageAfter("ideate")).toBeUndefined(); expect(nextStageAfter("brainstorm")).toBe("plan"); expect(nextStageAfter("plan")).toBe("work"); - expect(nextStageAfter("work")).toBeUndefined(); + expect(nextStageAfter("work")).toBe("compound"); + expect(nextStageAfter("compound")).toBeUndefined(); expect(nextStageAfter("debug")).toBeUndefined(); }); @@ -47,7 +48,7 @@ describe("compound engineering stage registry", () => { expect(stage).toMatchObject({ stageId: "debug", - order: 600, + order: 700, skillId: "ce-debug", artifactLocation: "docs/debug/", artifactGlob: "docs/debug/**/*.md", diff --git a/plugins/fusion-plugin-compound-engineering/src/__tests__/sync.test.ts b/plugins/fusion-plugin-compound-engineering/src/__tests__/sync.test.ts index 89f0335f9d..d2eb74da90 100644 --- a/plugins/fusion-plugin-compound-engineering/src/__tests__/sync.test.ts +++ b/plugins/fusion-plugin-compound-engineering/src/__tests__/sync.test.ts @@ -183,21 +183,37 @@ describe("U8 reconciler (convergence + outbound)", () => { expect(state.currentStage).toBe("work"); }); - it("completing work terminates the automatic pipeline without creating a debug task", async () => { + it("completing work advances to Compound without creating a debug task", async () => { const { cePipelineId, task } = await landPipeline("work"); const before = await taskStore.listTasks(); await moveTo(task.id, "done"); const result = await reconcileCePipelines(ctx); + expect(result.advanced).toBe(1); + expect(result.tasksCreated).toBe(1); + expect(getCePipelineStore(ctx).getState(cePipelineId)).toMatchObject({ + currentStage: "compound", + status: "awaiting_board", + }); + expect(await taskStore.listTasks()).toHaveLength(before.length + 1); + expect(getCePipelineStore(ctx).listByPipeline(cePipelineId).some((link) => link.ceStageId === "debug")).toBe(false); + }); + + it("completing Compound closes the delivery loop without creating another task", async () => { + const { cePipelineId, task } = await landPipeline("compound"); + const before = await taskStore.listTasks(); + + await moveTo(task.id, "done"); + const result = await reconcileCePipelines(ctx); + expect(result.advanced).toBe(1); expect(result.tasksCreated).toBe(0); expect(getCePipelineStore(ctx).getState(cePipelineId)).toMatchObject({ - currentStage: "work", + currentStage: "compound", status: "completed", }); expect(await taskStore.listTasks()).toHaveLength(before.length); - expect(getCePipelineStore(ctx).listByPipeline(cePipelineId).some((link) => link.ceStageId === "debug")).toBe(false); }); it("outbound: advancing the pipeline propagates a NEW next-stage board task", async () => { diff --git a/plugins/fusion-plugin-compound-engineering/src/artifacts/__tests__/discovery.test.ts b/plugins/fusion-plugin-compound-engineering/src/artifacts/__tests__/discovery.test.ts index 9d6716683f..10bda3d56f 100644 --- a/plugins/fusion-plugin-compound-engineering/src/artifacts/__tests__/discovery.test.ts +++ b/plugins/fusion-plugin-compound-engineering/src/artifacts/__tests__/discovery.test.ts @@ -78,7 +78,7 @@ describe("discoverArtifacts", () => { expect(byStage.plan.label).toBe("Plan"); expect(byStage.work.entries[0]).toMatchObject({ path: "docs/work/work.md" }); expect(byStage.debug.entries[0]).toMatchObject({ path: "docs/debug/debug.md" }); - expect(byStage.solution.entries).toHaveLength(1); + expect(byStage.compound.entries).toHaveLength(1); // Every group present is flagged present. expect(byStage.ideate.present).toBe(true); expect(byStage.brainstorm.entries[0]).toMatchObject({ path: "docs/brainstorms/requirements.md" }); diff --git a/plugins/fusion-plugin-compound-engineering/src/artifacts/discovery.ts b/plugins/fusion-plugin-compound-engineering/src/artifacts/discovery.ts index 175bb835b4..deb3a46a43 100644 --- a/plugins/fusion-plugin-compound-engineering/src/artifacts/discovery.ts +++ b/plugins/fusion-plugin-compound-engineering/src/artifacts/discovery.ts @@ -65,7 +65,7 @@ function stageLocations(): ConventionalLocation[] { const path = definition.artifactLocation.replace(/\/$/, ""); return { stage: definition.stageId, - label: definition.label, + label: definition.stageId === "compound" ? "Learnings" : definition.label, path, kind: definition.artifactLocation.endsWith("/") ? "directory" : "file", }; @@ -76,7 +76,6 @@ function conventionalLocations(): ConventionalLocation[] { return [ ...stageLocations(), { stage: "brainstorm", label: "Brainstorms", path: "docs/brainstorms", kind: "directory" }, - { stage: "solution", label: "Solutions", path: "docs/solutions", kind: "directory" }, { stage: "concepts", label: "Concepts", path: "CONCEPTS.md", kind: "file" }, ]; } diff --git a/plugins/fusion-plugin-compound-engineering/src/dashboard/CompoundEngineeringView.css b/plugins/fusion-plugin-compound-engineering/src/dashboard/CompoundEngineeringView.css index d95afc69a8..22523b2fa6 100644 --- a/plugins/fusion-plugin-compound-engineering/src/dashboard/CompoundEngineeringView.css +++ b/plugins/fusion-plugin-compound-engineering/src/dashboard/CompoundEngineeringView.css @@ -132,6 +132,11 @@ The shared ViewHeader sits directly above plugin content, so the body needs top gap: calc(var(--space-sm) - var(--space-xs) / 2); } +/* Keep multi-run histories scannable; older artifacts remain available on demand. */ +.ce-group-more { border-top: 1px solid var(--border); padding-top: var(--space-xs); } +.ce-group-more summary { color: var(--text-muted); cursor: pointer; font-size: 0.74rem; font-weight: 600; } +.ce-group-more[open] summary { margin-bottom: var(--space-xs); color: var(--text); } + .ce-artifact { display: flex; align-items: center; @@ -748,21 +753,30 @@ The pipeline overview must communicate progression and the next useful action at .ce-pipeline-header h2 { margin: 0; font-size: 1rem; font-weight: 650; } .ce-pipeline-header p { margin: calc(var(--space-xs) / 2) 0 0; color: var(--text-muted); font-size: 0.78rem; } .ce-investigate { display: inline-flex; align-items: center; gap: var(--space-xs); flex: none; } -.ce-stage-rail { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); list-style: none; margin: 0; padding: 0; } +.ce-pipeline-block { display: flex; flex-direction: column; gap: var(--space-xs); } +.ce-pipeline-block-heading { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-md); } +.ce-pipeline-block-heading h3 { margin: 0; font-size: 0.78rem; font-weight: 650; } +.ce-pipeline-block-heading span { color: var(--text-dim); font-size: 0.7rem; } +.ce-stage-rail { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); list-style: none; margin: 0; padding: 0; } +.ce-stage-rail[data-count="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.ce-stage-rail[data-count="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); } .ce-stage-step { position: relative; min-width: 0; border-block: 1px solid var(--border); border-left: 1px solid var(--border); background: var(--surface); } .ce-stage-step:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); } .ce-stage-step:last-child { border-right: 1px solid var(--border); border-radius: 0 var(--radius-md) var(--radius-md) 0; } .ce-stage-step::after { position: absolute; z-index: 2; top: calc(var(--space-lg) + 1px); right: calc(var(--space-xs) * -1); width: calc(var(--space-sm) - 2px); height: calc(var(--space-sm) - 2px); border-top: 1px solid var(--border); border-right: 1px solid var(--border); background: inherit; content: ""; transform: rotate(45deg); } .ce-stage-step:last-child::after { display: none; } -.ce-stage-main { display: flex; width: 100%; min-height: calc(var(--space-2xl) * 2.25); align-items: flex-start; gap: var(--space-sm); border: 0; background: transparent; color: inherit; cursor: pointer; padding: var(--space-md); text-align: left; } +.ce-stage-main { display: flex; width: 100%; min-height: calc(var(--space-2xl) * 2.65); align-items: flex-start; gap: var(--space-sm); border: 0; background: transparent; color: inherit; cursor: pointer; padding: var(--space-md); text-align: left; } .ce-stage-main:hover:not(:disabled), .ce-stage-main:focus-visible { background: var(--card-hover); } .ce-stage-main:focus-visible { position: relative; z-index: 3; outline: 2px solid var(--color-info); outline-offset: -2px; } .ce-stage-icon { display: grid; width: calc(var(--space-lg) + var(--space-xs)); height: calc(var(--space-lg) + var(--space-xs)); flex: none; place-items: center; border: 1px solid var(--border); border-radius: 50%; color: var(--text-muted); } -.ce-stage-step[data-status="complete"] .ce-stage-icon { border-color: color-mix(in srgb, var(--color-success) 45%, var(--border)); color: var(--color-success); } +.ce-stage-step[data-status="current"] .ce-stage-icon, +.ce-stage-step[data-status="history"] .ce-stage-icon, +.ce-stage-step[data-status="last-run-complete"] .ce-stage-icon { border-color: color-mix(in srgb, var(--color-success) 45%, var(--border)); color: var(--color-success); } .ce-stage-step[data-status="active"] .ce-stage-icon, .ce-stage-step[data-status="needs-input"] .ce-stage-icon { border-color: var(--color-info); background: color-mix(in srgb, var(--color-info) 12%, var(--surface)); color: var(--color-info); } .ce-stage-step[data-status="needs-input"] .ce-stage-status { color: var(--color-warning); } .ce-stage-copy { display: flex; min-width: 0; flex-direction: column; gap: calc(var(--space-xs) / 2); } .ce-stage-copy strong { overflow: hidden; font-size: 0.86rem; text-overflow: ellipsis; white-space: nowrap; } +.ce-stage-purpose { color: var(--text-muted); font-size: 0.7rem; line-height: 1.25; } .ce-stage-status { color: var(--text-muted); font-size: 0.7rem; } .ce-stage-artifact { display: flex; width: calc(100% - var(--space-lg)); min-width: 0; align-items: center; gap: var(--space-xs); margin: 0 var(--space-sm) var(--space-sm); border: 0; background: transparent; color: var(--text-dim); cursor: pointer; font: inherit; font-size: 0.68rem; text-align: left; } .ce-stage-artifact span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } @@ -793,7 +807,8 @@ The pipeline overview must communicate progression and the next useful action at @media (max-width: 768px) { .ce-pipeline-header { align-items: center; } - .ce-stage-rail { grid-template-columns: 1fr; } + .ce-stage-rail, + .ce-stage-rail[data-count] { grid-template-columns: 1fr; } .ce-stage-step, .ce-stage-step:first-child, .ce-stage-step:last-child { border: 1px solid var(--border); border-bottom: 0; border-radius: 0; } .ce-stage-step:first-child { border-radius: var(--radius-md) var(--radius-md) 0 0; } .ce-stage-step:last-child { border-bottom: 1px solid var(--border); border-radius: 0 0 var(--radius-md) var(--radius-md); } diff --git a/plugins/fusion-plugin-compound-engineering/src/dashboard/CompoundEngineeringView.tsx b/plugins/fusion-plugin-compound-engineering/src/dashboard/CompoundEngineeringView.tsx index f4771f9ae8..dc1700ef52 100644 --- a/plugins/fusion-plugin-compound-engineering/src/dashboard/CompoundEngineeringView.tsx +++ b/plugins/fusion-plugin-compound-engineering/src/dashboard/CompoundEngineeringView.tsx @@ -170,15 +170,18 @@ function SessionsPanel({ } function nextPipelineStageId(stageId: string): string | undefined { + if (stageId === "strategy" || stageId === "ideate") return "brainstorm"; const stages = listPipelineStages(); const index = stages.findIndex((stage) => stage.stageId === stageId); return index >= 0 ? stages[index + 1]?.stageId : undefined; } -type RailStatus = "Not started" | "Active" | "Needs input" | "Complete"; +type RailStatus = "Not started" | "Active" | "Needs input" | "Last run complete" | "History" | "Current"; function artifactsForStage(stageId: string, groups: CeArtifactGroup[]): CeArtifactEntry[] { - const entries = groups.find((group) => group.stage === stageId)?.entries ?? []; + const entries = groups.find((group) => group.stage === stageId)?.entries + ?? (stageId === "compound" ? groups.find((group) => group.stage === "solution")?.entries : undefined) + ?? []; if (stageId === "brainstorm") { const unifiedEntries = groups.find((group) => group.stage === "plan")?.entries ?? []; return [...entries, ...unifiedEntries.filter((entry) => ( @@ -200,16 +203,87 @@ function railStatus(stageId: string, sessions: CeSession[], artifact?: CeArtifac const latest = sessions.find((session) => session.stage === stageId); if (latest?.status === "awaiting_input") return "Needs input"; if (latest?.status === "active" || latest?.status === "launching") return "Active"; - if (latest?.status === "completed" || artifact?.kind === "artifact") return "Complete"; + if (latest?.status === "completed") return "Last run complete"; + if (artifact?.kind === "artifact") return stageId === "strategy" ? "Current" : "History"; return "Not started"; } +const STAGE_PURPOSE: Record = { + strategy: "Set the product direction", + ideate: "Explore high-leverage ideas", + brainstorm: "Define what to build", + plan: "Decide how to build it", + work: "Build, simplify, and review", + compound: "Save reusable learnings", +}; + +function StageRail({ + stages, + groups, + sessions, + disabled, + openFile, + onLaunch, + onOpenSession, +}: { + stages: CeStageDefinition[]; + groups: CeArtifactGroup[]; + sessions: CeSession[]; + disabled: boolean; + openFile?: PluginDashboardViewContext["openFile"]; + onLaunch: (stage: CeStageDefinition) => void; + onOpenSession: (session: CeSession) => void; +}) { + return ( +
    + {stages.map((stage) => { + const artifacts = artifactsForStage(stage.stageId, groups); + const artifact = artifacts[0]; + const stageSessions = sessions.filter((session) => session.stage === stage.stageId); + const latestSession = stageSessions[0]; + const status = railStatus(stage.stageId, sessions, artifact); + const Icon = resolveIcon(stage.icon); + return ( +
  1. + + {artifact?.kind === "artifact" ? ( + + ) : No artifact yet} +
  2. + ); + })} +
+ ); +} + /** * FNXC:CompoundEngineeringUI 2026-07-10-12:00: - * Operators need the five-stage compounding loop as the overview's primary navigation. Stages report repeatable run and artifact collections instead of implying one permanent completion; Strategy remains the upstream singleton anchor, while Debug remains a separate investigation action because it is not pipeline progression. + * Operators need the optional foundation plus four-stage delivery loop as the overview's primary navigation. Stages report repeatable run and artifact collections instead of implying one permanent completion; Strategy remains the upstream singleton anchor, while Debug remains a separate investigation action because it is not pipeline progression. * * FNXC:CompoundEngineeringUI 2026-07-10-23:40: - * Compound Engineering follows the upstream repeatable loop: brainstorm requirements, plan implementation, work, review, compound, then repeat with better context. Brainstorm and Plan therefore expose collection counts and latest activity over multiple unified-plan files rather than a single artifact slot. + * Compound Engineering follows the upstream repeatable loop: brainstorm requirements, plan implementation, work through built-in simplification and review, compound the learning, then repeat with better context. Brainstorm and Plan therefore expose collection counts and latest activity over multiple unified-plan files rather than a single artifact slot. + * + * FNXC:CompoundEngineeringUI 2026-07-11-00:02: + * Strategy and Ideate are optional foundation work, not mandatory delivery gates. Visually separate them from the Brainstorm -> Plan -> Work -> Compound loop, and state that Work owns simplification and review so users do not repeat quality stages manually. */ function PipelineOverview({ groups, @@ -226,14 +300,16 @@ function PipelineOverview({ onLaunch: (stage: CeStageDefinition) => void; onOpenSession: (session: CeSession) => void; }) { - const stages = listPipelineStages(); + const stages = listStages().filter((stage) => stage.stageId !== "debug"); + const foundationStages = stages.filter((stage) => stage.stageId === "strategy" || stage.stageId === "ideate"); + const deliveryStages = stages.filter((stage) => stage.stageId !== "strategy" && stage.stageId !== "ideate"); const debug = getStage("debug"); return (
-

Pipeline

-

Move from direction to delivered work.

+

Compound Engineering

+

Plan deeply, ship carefully, and save what you learn.

{debug ? ( ) : null}
-
    - {stages.map((stage) => { - const artifacts = artifactsForStage(stage.stageId, groups); - const artifact = artifacts[0]; - const stageSessions = sessions.filter((session) => session.stage === stage.stageId); - const latestSession = stageSessions[0]; - const status = railStatus(stage.stageId, sessions, artifact); - const Icon = resolveIcon(stage.icon); - return ( -
  1. - - {artifact?.kind === "artifact" ? ( - - ) : No artifact yet} -
  2. - ); - })} -
+
+

Foundation

Optional grounding
+ +
+
+

Delivery loop

Repeat for each meaningful change
+ +
); } @@ -302,11 +350,11 @@ function EmptyState({ onStart }: { onStart: () => void }) {

Start your compounding pipeline

No compound-engineering artifacts found yet. Compound Engineering tracks the documents your - pipeline produces — strategy, ideation, brainstorms, plans, solutions, and concepts — as you + workflow produces — strategy, ideation, brainstorms, plans, work logs, learnings, and concepts — as you move through each stage.

- Begin with a stage and its artifact will appear here, grouped and traceable. + Start with Brainstorm when the problem is still fuzzy, Plan when requirements are settled, or Debug for broken behavior.