diff --git a/.changeset/clean-compound-pipeline.md b/.changeset/clean-compound-pipeline.md index db3e98db3b..104f7734e9 100644 --- a/.changeset/clean-compound-pipeline.md +++ b/.changeset/clean-compound-pipeline.md @@ -2,6 +2,6 @@ "@runfusion/fusion": minor --- -summary: Clarify Compound Engineering progress and preserve one plan from brainstorm through delivery. +summary: Support repeatable Compound Engineering cycles with project-scoped progress and artifact collections. category: feature -dev: Adds a stage rail, safer session controls, explicit choice confirmation, and terminal Work progression. +dev: Adds collection-aware stages, safer session controls, explicit choice confirmation, and terminal Work progression. diff --git a/docs/screenshots/compound-engineering/pipeline-desktop.png b/docs/screenshots/compound-engineering/pipeline-desktop.png index 75b60c1ef8..7791a07b2f 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:5e4dc3baed78f895c3a954ade947e3ee211da7612b6f1693127f8f67f74689a3 -size 234370 +oid sha256:a6c218824cbd21ec36305437e2adc5c810163ab25cd28fdee705bb12ddf48b93 +size 270173 diff --git a/docs/screenshots/compound-engineering/pipeline-mobile.png b/docs/screenshots/compound-engineering/pipeline-mobile.png index db76d5b1c7..668a8fc751 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:7e1320ecaac17cc84399f7cf58de13df5100175dd57e34fd1d6c0039e630f487 -size 90474 +oid sha256:9bbcbf6767d6982ebb4340498396dc1453eb2e7458c3f0877c20d7a4f5ece9d3 +size 95990 diff --git a/plugins/fusion-plugin-compound-engineering/src/__tests__/session-routes.test.ts b/plugins/fusion-plugin-compound-engineering/src/__tests__/session-routes.test.ts index 76ec40d4d6..74ab72cba9 100644 --- a/plugins/fusion-plugin-compound-engineering/src/__tests__/session-routes.test.ts +++ b/plugins/fusion-plugin-compound-engineering/src/__tests__/session-routes.test.ts @@ -133,6 +133,20 @@ describe("session routes (polling transport)", () => { expect(sessions.map((s) => s.stage).sort()).toEqual(["brainstorm", "plan"]); }); + it("GET /sessions scopes every session consumer to the requested project", async () => { + const { getCeSessionStore } = await import("../session/session-store.js"); + const store = getCeSessionStore(h.ctx); + const projectA = store.create({ stage: "brainstorm", projectId: "project-a" }); + store.create({ stage: "plan", projectId: "project-b" }); + store.create({ stage: "debug" }); + + const res = await call("GET", "/sessions", { params: {}, query: { projectId: "project-a" } }, h.ctx); + + expect(res.status).toBe(200); + const sessions = (res.body as { sessions: Array<{ id: string; projectId: string | null }> }).sessions; + expect(sessions).toEqual([expect.objectContaining({ id: projectA.id, projectId: "project-a" })]); + }); + it("GET /sessions keeps error, interrupted, awaiting_input, active, and completed rows independently manageable", async () => { const { getCeSessionStore } = await import("../session/session-store.js"); const store = getCeSessionStore(h.ctx); 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 7c6abefb48..9d6716683f 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 @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import * as realFs from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { registerStage } from "../../session/stage-registry.js"; +import { registerStage, unregisterStage } from "../../session/stage-registry.js"; // Mock node:fs so we can observe/inject behaviour around readFileSync and // accessSync without relying on vi.spyOn (ESM namespace exports are not @@ -40,6 +40,7 @@ describe("discoverArtifacts", () => { afterEach(() => { if (root) rmSync(root, { recursive: true, force: true }); + unregisterStage("publish-check"); readFileHook = undefined; accessHook = undefined; vi.restoreAllMocks(); @@ -52,6 +53,8 @@ describe("discoverArtifacts", () => { mkdirSync(join(root, "docs/ideation"), { recursive: true }); writeFileSync(join(root, "docs/ideation/a.md"), "ideation a"); writeFileSync(join(root, "docs/ideation/b.md"), "ideation b"); + mkdirSync(join(root, "docs/brainstorms"), { recursive: true }); + writeFileSync(join(root, "docs/brainstorms/requirements.md"), "requirements"); mkdirSync(join(root, "docs/plans"), { recursive: true }); writeFileSync(join(root, "docs/plans/plan1.md"), "plan 1"); mkdirSync(join(root, "docs/work"), { recursive: true }); @@ -64,20 +67,21 @@ describe("discoverArtifacts", () => { const result = discoverArtifacts(root); const byStage = Object.fromEntries(result.groups.map((g) => [g.stage, g])); - expect(result.totalArtifacts).toBe(8); + expect(result.totalArtifacts).toBe(9); expect(result.totalErrors).toBe(0); expect(byStage.strategy.entries).toHaveLength(1); expect(byStage.concepts.entries).toHaveLength(1); expect(byStage.ideate.entries).toHaveLength(2); expect(byStage.plan.entries).toHaveLength(1); expect(byStage.plan.entries[0]).toMatchObject({ path: "docs/plans/plan1.md" }); - expect(byStage.plan.label).toBe("Brainstorm / Plan"); + expect(byStage.brainstorm.entries).toHaveLength(1); + 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); // Every group present is flagged present. expect(byStage.ideate.present).toBe(true); - expect(byStage.brainstorm).toBeUndefined(); + expect(byStage.brainstorm.entries[0]).toMatchObject({ path: "docs/brainstorms/requirements.md" }); // All entries are artifacts in the happy path. expect(result.groups.flatMap((g) => g.entries).every((e) => e.kind === "artifact")).toBe(true); }); @@ -112,7 +116,7 @@ describe("discoverArtifacts", () => { expect(populated.map((g) => g.stage).sort()).toEqual(["plan", "strategy"]); expect(empty.length).toBeGreaterThan(0); // Empty groups are still present in the result so the hub can render them. - expect(result.groups).toHaveLength(7); + expect(result.groups).toHaveLength(8); }); it("returns an all-empty result when nothing is present (first-run)", () => { diff --git a/plugins/fusion-plugin-compound-engineering/src/artifacts/discovery.ts b/plugins/fusion-plugin-compound-engineering/src/artifacts/discovery.ts index 3df21c7f33..175bb835b4 100644 --- a/plugins/fusion-plugin-compound-engineering/src/artifacts/discovery.ts +++ b/plugins/fusion-plugin-compound-engineering/src/artifacts/discovery.ts @@ -55,14 +55,17 @@ interface ConventionalLocation { */ /* FNXC:CompoundEngineeringArtifacts 2026-07-10-12:00: -The artifact hub must follow registered stage output locations so Work and Debug remain discoverable as the pipeline evolves. Brainstorm and Plan share one durable docs/plans collection; exposing the removed docs/brainstorms location or two groups for the same path misrepresents the unified-plan contract. +The artifact hub must follow registered stage output locations so Work and Debug remain discoverable as the pipeline evolves. + +FNXC:CompoundEngineeringArtifacts 2026-07-10-23:40: +The upstream Compound Engineering workflow writes repeatable requirements documents to docs/brainstorms and implementation plans to docs/plans. Discovery must retain the dedicated requirements history while also supporting requirements-only unified plans produced by newer in-place handoffs. */ function stageLocations(): ConventionalLocation[] { return listStages().filter((definition) => definition.stageId !== "brainstorm").map((definition) => { const path = definition.artifactLocation.replace(/\/$/, ""); return { stage: definition.stageId, - label: definition.stageId === "plan" ? "Brainstorm / Plan" : definition.label, + label: definition.label, path, kind: definition.artifactLocation.endsWith("/") ? "directory" : "file", }; @@ -72,6 +75,7 @@ function stageLocations(): ConventionalLocation[] { 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 f803768e6c..d95afc69a8 100644 --- a/plugins/fusion-plugin-compound-engineering/src/dashboard/CompoundEngineeringView.css +++ b/plugins/fusion-plugin-compound-engineering/src/dashboard/CompoundEngineeringView.css @@ -83,6 +83,20 @@ The shared ViewHeader sits directly above plugin content, so the body needs top gap: var(--space-sm); } +/* Strategy is one durable upstream anchor; keep it as a compact row while repeatable workflow outputs retain collection cards. */ +.ce-group[data-layout="singleton"] { + grid-column: 1 / -1; + display: grid; + grid-template-columns: minmax(7rem, 0.3fr) minmax(0, 1fr); + align-items: center; + column-gap: var(--space-lg); + padding-block: var(--space-sm); +} + +.ce-group[data-layout="singleton"] .ce-artifact-list { + min-width: 0; +} + .ce-group[data-empty="true"] { color: var(--text-muted); } @@ -179,6 +193,11 @@ The shared ViewHeader sits directly above plugin content, so the body needs top padding: var(--space-sm); } +.ce-view[data-mobile="true"] .ce-group[data-layout="singleton"] { + grid-template-columns: 1fr; + row-gap: var(--space-xs); +} + .ce-view[data-mobile="true"] .ce-groups { grid-template-columns: 1fr; } diff --git a/plugins/fusion-plugin-compound-engineering/src/dashboard/CompoundEngineeringView.tsx b/plugins/fusion-plugin-compound-engineering/src/dashboard/CompoundEngineeringView.tsx index 7efa9c7849..f4771f9ae8 100644 --- a/plugins/fusion-plugin-compound-engineering/src/dashboard/CompoundEngineeringView.tsx +++ b/plugins/fusion-plugin-compound-engineering/src/dashboard/CompoundEngineeringView.tsx @@ -177,23 +177,23 @@ function nextPipelineStageId(stageId: string): string | undefined { type RailStatus = "Not started" | "Active" | "Needs input" | "Complete"; -function artifactForStage(stageId: string, groups: CeArtifactGroup[]): CeArtifactEntry | undefined { - const groupId = stageId === "brainstorm" ? "plan" : stageId; - const entries = groups.find((group) => group.stage === groupId)?.entries ?? []; +function artifactsForStage(stageId: string, groups: CeArtifactGroup[]): CeArtifactEntry[] { + const entries = groups.find((group) => group.stage === stageId)?.entries ?? []; if (stageId === "brainstorm") { - return entries.find((entry) => ( + const unifiedEntries = groups.find((group) => group.stage === "plan")?.entries ?? []; + return [...entries, ...unifiedEntries.filter((entry) => ( entry.kind === "artifact" && entry.artifactContract === "ce-unified-plan/v1" - && (entry.artifactReadiness === "requirements-only" || entry.artifactReadiness === "implementation-ready") - )); + && (entry.artifactReadiness === "requirements-only" || !entry.productContractSource || entry.productContractSource === "ce-brainstorm") + ))]; } if (stageId === "plan") { - return entries.find((entry) => ( + return entries.filter((entry) => ( entry.kind === "artifact" && (entry.artifactReadiness === "implementation-ready" || !entry.artifactReadiness) )); } - return entries.find((entry) => entry.kind === "artifact"); + return entries.filter((entry) => entry.kind === "artifact"); } function railStatus(stageId: string, sessions: CeSession[], artifact?: CeArtifactEntry): RailStatus { @@ -206,7 +206,10 @@ function railStatus(stageId: string, sessions: CeSession[], artifact?: CeArtifac /** * FNXC:CompoundEngineeringUI 2026-07-10-12:00: - * Operators need the five-stage compounding pipeline as the overview's primary navigation. Each stage shows actionable state and its latest durable artifact, while Debug remains a separate investigation action because it is not pipeline progression. + * 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. + * + * 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. */ function PipelineOverview({ groups, @@ -240,8 +243,10 @@ function PipelineOverview({