From b09ab03aae0cfd7f98cd30b823f210a20a0f3669 Mon Sep 17 00:00:00 2001 From: jwthewes Date: Wed, 10 Jun 2026 14:30:10 +0200 Subject: [PATCH 1/3] fix: standardize sprint workflow state --- .../components/observability/PhaseBlock.tsx | 24 +++--- .../observability/ProjectDiagram.tsx | 81 ++++++++++++++++++- .../lib/observability/fetchProjectInfos.ts | 10 ++- frontend/src/pages/ConstructionPage.tsx | 51 +++++++++++- frontend/src/pages/InceptionPage.tsx | 6 +- frontend/src/pages/ReviewPage.tsx | 6 +- frontend/src/services/sprints.ts | 3 + .../common/process-overview.md | 2 + .../common/question-format-guide.md | 8 +- .../common/session-continuity.md | 4 + .../common/terminology.md | 2 +- .../common/welcome-message.md | 9 ++- .../construction/build-and-test.md | 11 +-- .../construction/code-generation.md | 3 +- .../construction/functional-design.md | 3 +- .../construction/infrastructure-design.md | 3 +- .../construction/nfr-design.md | 3 +- .../construction/nfr-requirements.md | 3 +- .../inception/application-design.md | 3 +- .../inception/requirements-analysis.md | 3 +- .../inception/reverse-engineering.md | 3 +- .../inception/units-generation.md | 11 +-- .../inception/user-stories.md | 3 +- .../inception/workflow-planning.md | 1 + .../inception/workspace-detection.md | 1 + .../aws-aidlc-rules/core-workflow.md | 22 ++++- lambda/agents-ecs/mcp-server-graph/index.js | 16 +++- lambda/sprint-graph/index.js | 19 ++++- lambda/sprint-graph/test/sprint-graph.test.js | 38 +++++++-- lambda/sprints/index.js | 37 ++++++++- lambda/sprints/test/sprints.test.js | 39 +++++++++ 31 files changed, 366 insertions(+), 62 deletions(-) diff --git a/frontend/src/components/observability/PhaseBlock.tsx b/frontend/src/components/observability/PhaseBlock.tsx index 737de10e..83e185e6 100644 --- a/frontend/src/components/observability/PhaseBlock.tsx +++ b/frontend/src/components/observability/PhaseBlock.tsx @@ -18,6 +18,7 @@ interface Props { isFuturePhase: boolean; isUnlocked: boolean; agentStatus?: string | null; + activeStageKey?: string | null; progress: SprintProgress | null; sprint: Sprint | null; taskStatuses?: TaskAgentStatus[]; @@ -33,6 +34,7 @@ export function PhaseBlock({ isFuturePhase, isUnlocked, agentStatus, + activeStageKey, progress, taskStatuses = [], prUrl, @@ -116,14 +118,16 @@ export function PhaseBlock({ {/* Rangée principale — avec flèches entre les steps */}
{config.mainSteps.map((step, idx) => { - const isFirstIncompleteMandatory = - isCurrentPhase && - agentStatus === 'running' && - step.mandatory && - !stepsDone.has(step.key) && - !config.mainSteps.some( - (s) => s.mandatory && !stepsDone.has(s.key) && config.mainSteps.indexOf(s) < idx, - ); + const isActive = activeStageKey + ? activeStageKey === step.key + : isCurrentPhase && + agentStatus === 'running' && + step.mandatory && + !stepsDone.has(step.key) && + !config.mainSteps.some( + (s) => + s.mandatory && !stepsDone.has(s.key) && config.mainSteps.indexOf(s) < idx, + ); return (
{idx > 0 && } @@ -131,7 +135,7 @@ export function PhaseBlock({ step={step} isDone={stepsDone.has(step.key)} isSkipped={stepsSkipped.has(step.key)} - isActive={isFirstIncompleteMandatory} + isActive={isActive} isCurrentPhase={isCurrentPhase} isPastPhase={isPastPhase} mandatoryBg={config.mandatoryBg} @@ -151,7 +155,7 @@ export function PhaseBlock({ step={step} isDone={stepsDone.has(step.key)} isSkipped={stepsSkipped.has(step.key)} - isActive={false} + isActive={activeStageKey === step.key} isCurrentPhase={isCurrentPhase} isPastPhase={isPastPhase} mandatoryBg={config.mandatoryBg} diff --git a/frontend/src/components/observability/ProjectDiagram.tsx b/frontend/src/components/observability/ProjectDiagram.tsx index 70b03531..6782c049 100644 --- a/frontend/src/components/observability/ProjectDiagram.tsx +++ b/frontend/src/components/observability/ProjectDiagram.tsx @@ -7,11 +7,75 @@ import type { ProjectAgentInfo, SprintProgress, VelocityMetrics } from '@/hooks/ import type { Sprint } from '@/services/sprints'; import type { TaskAgentStatus } from '@/services/agents'; +function hasConstructionEvidence( + sprint: Sprint | null, + progress: SprintProgress | null, + taskStatuses: TaskAgentStatus[], +): boolean { + return Boolean( + sprint && + (sprint.phase === 'CONSTRUCTION' || + sprint.currentAgentType === 'construction-orchestrator' || + sprint.currentAgentType === 'construction' || + sprint.branch || + sprint.prUrl || + (progress?.codeFileCount ?? 0) > 0 || + taskStatuses.length > 0), + ); +} + +function getEffectivePhase( + sprint: Sprint | null, + progress: SprintProgress | null, + taskStatuses: TaskAgentStatus[], +): PhaseKey | undefined { + if (!sprint) return undefined; + if (sprint.phase === 'INCEPTION' && hasConstructionEvidence(sprint, progress, taskStatuses)) { + return 'CONSTRUCTION'; + } + return sprint.phase as PhaseKey; +} + +function getEffectivePhaseStatus( + sprint: Sprint | null, + progress: SprintProgress | null, + taskStatuses: TaskAgentStatus[], +): string | null { + if (!sprint) return null; + if (sprint.phaseStatus) return sprint.phaseStatus; + if (sprint.phase === 'INCEPTION' && (progress?.taskCount ?? 0) > 0) return 'ready_for_transition'; + if ( + hasConstructionEvidence(sprint, progress, taskStatuses) && + (sprint.prUrl || + (taskStatuses.length > 0 && taskStatuses.every((t) => t.executionStatus === 'SUCCEEDED'))) + ) { + return 'ready_for_transition'; + } + return null; +} + +function normalizeStageKey(stage: string | null | undefined): string | null { + return stage ? stage.replaceAll('-', '_') : null; +} + +function getActiveStageKey( + phase: PhaseKey, + sprint: Sprint | null, + effectivePhase: PhaseKey | undefined, + effectivePhaseStatus: string | null, +): string | null { + if (!sprint || effectivePhase !== phase || effectivePhaseStatus === 'ready_for_transition') { + return null; + } + return normalizeStageKey(sprint.currentStage); +} + function getStepsDone( phase: PhaseKey, progress: SprintProgress | null, sprint: Sprint | null, taskStatuses: TaskAgentStatus[], + effectivePhaseStatus: string | null, ): Set { const done = new Set(); if (!sprint) return done; @@ -25,6 +89,9 @@ function getStepsDone( done.add('workflow_planning'); done.add('units_generation'); } + if (sprint.phase === 'INCEPTION' && effectivePhaseStatus === 'ready_for_transition') { + done.add('units_generation'); + } } if (phase === 'CONSTRUCTION') { @@ -35,6 +102,9 @@ function getStepsDone( taskStatuses.length > 0 && taskStatuses.every((t) => t.executionStatus === 'SUCCEEDED'); if (hasRunningOrDone || progress?.codeFileCount) done.add('code_generation'); if (allDone || sprint.prUrl) done.add('build_and_test'); + if (phase === 'CONSTRUCTION' && effectivePhaseStatus === 'ready_for_transition') { + done.add('build_and_test'); + } } if (phase === 'REVIEW') { @@ -78,7 +148,8 @@ export function ProjectDiagram({ }: Props) { const { project, sprint, progress, taskStatuses } = info; const agentStatus = sprint?.currentAgentStatus; - const currentPhase = sprint?.phase as PhaseKey | undefined; + const currentPhase = getEffectivePhase(sprint ?? null, progress, taskStatuses); + const effectivePhaseStatus = getEffectivePhaseStatus(sprint ?? null, progress, taskStatuses); const currentPhaseIdx = currentPhase ? PHASE_ORDER.indexOf(currentPhase) : -1; const handleClick = () => { @@ -149,8 +220,15 @@ export function ProjectDiagram({ progress, sprint ?? null, phase.key === 'CONSTRUCTION' ? taskStatuses : [], + effectivePhaseStatus, ); const stepsSkipped = getStepsSkipped(phase.key, stepsDone, isPastPhase, phase.steps); + const activeStageKey = getActiveStageKey( + phase.key, + sprint ?? null, + currentPhase, + effectivePhaseStatus, + ); return (
{phaseIdx > 0 && ( @@ -169,6 +247,7 @@ export function ProjectDiagram({ sprint !== null && (currentPhaseIdx === -1 || currentPhaseIdx >= phaseIdx) } agentStatus={agentStatus} + activeStageKey={activeStageKey} progress={progress} sprint={sprint ?? null} taskStatuses={phase.key === 'CONSTRUCTION' ? taskStatuses : []} diff --git a/frontend/src/lib/observability/fetchProjectInfos.ts b/frontend/src/lib/observability/fetchProjectInfos.ts index 2e4b282e..70a4d5b3 100644 --- a/frontend/src/lib/observability/fetchProjectInfos.ts +++ b/frontend/src/lib/observability/fetchProjectInfos.ts @@ -47,7 +47,15 @@ export async function fetchProjectInfos(l2: L2Intelligence): Promise 0; + + if (hasConstructionEvidence) { try { const { tasks } = await agentsService.getTaskAgentStatuses(project.id, latest.id); taskStatuses = tasks; diff --git a/frontend/src/pages/ConstructionPage.tsx b/frontend/src/pages/ConstructionPage.tsx index 664094f2..7379a39f 100644 --- a/frontend/src/pages/ConstructionPage.tsx +++ b/frontend/src/pages/ConstructionPage.tsx @@ -127,6 +127,35 @@ export default function ConstructionPage() { .catch(() => {}); }, [projectId, sprintId, sprint?.phase]); + // Backfill older sprints that started construction before the Sprint + // workflow-state contract existed. If construction evidence already exists, + // normalize the UI-owned phase so the dashboard no longer shows Inception as active. + useEffect(() => { + if (!projectId || !sprintId || !sprint || sprint.phase === 'CONSTRUCTION') return; + const hasConstructionEvidence = + sprint.currentAgentType === 'construction-orchestrator' || + sprint.currentAgentType === 'construction' || + codeFiles.length > 0 || + Boolean(sprint.branch); + if (!hasConstructionEvidence) return; + + sprintsService + .update(projectId, sprintId, { + phase: 'CONSTRUCTION', + currentStage: sprint.currentStage || 'code-generation', + phaseStatus: sprint.phaseStatus || 'active', + }) + .then(() => { + realtimeService.send('broadcastToDocument', { + documentId: `sprint:${sprintId}`, + action: 'sprint.phaseChanged', + data: { phase: 'CONSTRUCTION', sprintId }, + }); + return reload(); + }) + .catch(() => {}); + }, [codeFiles.length, projectId, reload, sprint, sprintId]); + // Reload on agent artifacts useEffect(() => { if (agentStatus.artifactsUpdated > 0) reload(); @@ -170,8 +199,20 @@ export default function ConstructionPage() { setStartingConstruction(true); setShowBranchSelector(false); try { - // Persist branch on sprint so subsequent runs skip the BranchSelector - await sprintsService.update(projectId, sprintId, { branch, baseBranch }); + // Construction kick-off is a UI-owned phase transition. Normalize older + // sprints that reached this page before workflow state fields existed. + await sprintsService.update(projectId, sprintId, { + phase: 'CONSTRUCTION', + currentStage: 'code-generation', + phaseStatus: 'active', + branch, + baseBranch, + }); + realtimeService.send('broadcastToDocument', { + documentId: `sprint:${sprintId}`, + action: 'sprint.phaseChanged', + data: { phase: 'CONSTRUCTION', sprintId }, + }); const result = await agentsService.startWorkflow(projectId, { phase: 'construction-orchestrator', sprintId, @@ -283,7 +324,11 @@ export default function ConstructionPage() { const handleApprovePhase = async () => { setApprovingPhase(true); try { - await sprintsService.update(projectId, sprintId, { phase: 'REVIEW' }); + await sprintsService.update(projectId, sprintId, { + phase: 'REVIEW', + currentStage: null, + phaseStatus: 'active', + }); realtimeService.send('broadcastToDocument', { documentId: `sprint:${sprintId}`, action: 'sprint.phaseChanged', diff --git a/frontend/src/pages/InceptionPage.tsx b/frontend/src/pages/InceptionPage.tsx index 1dbeb25b..42e40676 100644 --- a/frontend/src/pages/InceptionPage.tsx +++ b/frontend/src/pages/InceptionPage.tsx @@ -311,7 +311,11 @@ export default function InceptionPage() { const handleApprovePhase = async () => { setApprovingPhase(true); try { - await sprintsService.update(projectId, sprintId, { phase: 'CONSTRUCTION' }); + await sprintsService.update(projectId, sprintId, { + phase: 'CONSTRUCTION', + currentStage: null, + phaseStatus: 'active', + }); realtimeService.send('broadcastToDocument', { documentId: `sprint:${sprintId}`, action: 'sprint.phaseChanged', diff --git a/frontend/src/pages/ReviewPage.tsx b/frontend/src/pages/ReviewPage.tsx index 06623dfd..a1432be7 100644 --- a/frontend/src/pages/ReviewPage.tsx +++ b/frontend/src/pages/ReviewPage.tsx @@ -946,7 +946,11 @@ export default function ReviewPage() { body, }); if (review.status === 'PASSED') { - await sprintsService.update(projectId, sprintId, { phase: 'COMPLETED' }); + await sprintsService.update(projectId, sprintId, { + phase: 'COMPLETED', + currentStage: null, + phaseStatus: 'completed', + }); realtimeService.send('broadcastToDocument', { documentId: `sprint:${sprintId}`, action: 'sprint.phaseChanged', diff --git a/frontend/src/services/sprints.ts b/frontend/src/services/sprints.ts index befc8fca..51b13198 100644 --- a/frontend/src/services/sprints.ts +++ b/frontend/src/services/sprints.ts @@ -1,6 +1,7 @@ import { api } from './api'; export type SprintPhase = 'INCEPTION' | 'CONSTRUCTION' | 'REVIEW' | 'COMPLETED'; +export type SprintPhaseStatus = 'active' | 'ready_for_transition' | 'completed' | null; export type AgentStatus = 'running' | 'waiting' | 'completed' | 'failed' | 'cancelled' | null; // Polymorphic link from a Sprint to the tracker resource it was started from. @@ -30,6 +31,8 @@ export interface Sprint { name: string; description: string; phase: SprintPhase; + currentStage: string | null; + phaseStatus: SprintPhaseStatus; createdAt: string; // Agent state fields (Phase 1 & 2) currentExecutionArn: string | null; diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/process-overview.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/process-overview.md index aef1a52c..a910e8e6 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/process-overview.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/process-overview.md @@ -157,5 +157,7 @@ flowchart TD - Simple changes may skip conditional INCEPTION stages - Complex changes get full INCEPTION and CONSTRUCTION treatment - **Graph database is the single source of truth** for all artifacts +- **Sprint node is the single source of truth for workflow state** (`phase`, `current_stage`, `phase_status`) - **`ask_question` is the only collaboration mechanism** for human input +- **Do not use `ask_question` for phase transitions** — phase transitions are UI-owned - **Carried-forward artifacts** maintain traceability to their source sprint via CARRIED_FROM edges diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/question-format-guide.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/question-format-guide.md index 7e1332ba..d002696e 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/question-format-guide.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/question-format-guide.md @@ -137,16 +137,16 @@ The tool blocks until clarification is received. ### Approval Gates -For stage completion approvals: +For intra-phase stage completion approvals: ``` Call `ask_question` with: { questions: [{ - text: "Requirements analysis is complete. I identified 5 functional requirements and 3 non-functional requirements:\n- [brief summary of key requirements]\n\nDo you approve to proceed to User Stories?", + text: "Requirements analysis is complete. I identified 5 functional requirements and 3 non-functional requirements:\n- [brief summary of key requirements]\n\nDo you approve these requirements so I can continue with User Stories inside Inception?", type: "single", options: [ - { label: "Approve", description: "Proceed to User Stories phase" }, + { label: "Approve", description: "Continue to the User Stories stage inside Inception" }, { label: "Request changes", description: "Describe what needs to change in the free text below" } ] }] @@ -155,6 +155,8 @@ Call `ask_question` with: If the response is "Request changes" (with or without free text), treat it as a change request. Make the requested changes, then call `ask_question` again with the updated summary. +Do not use approval gates for phase-boundary transitions. Inception → Construction, Construction → Review, and Review → Completed are user-initiated UI actions. At a phase boundary, update the Sprint node (`current_stage` plus `phase_status: "ready_for_transition"`), provide a final summary, and exit without calling `ask_question`. + --- ## Option Design Guidelines diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/session-continuity.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/session-continuity.md index 91a49c91..3c5c6cc6 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/session-continuity.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/session-continuity.md @@ -14,6 +14,9 @@ From the Sprint node properties, determine: - **Current Phase**: INCEPTION, CONSTRUCTION, or REVIEW - **Current Stage**: The specific stage in progress +- **Phase Status**: `active`, `ready_for_transition`, or `completed` + +If `phase_status` is `ready_for_transition`, do not ask for phase-transition approval. Present the ready state and tell the team to use the Sprint page transition button when they are ready. From the presence of graph nodes, determine what has been completed: @@ -31,6 +34,7 @@ Present the current status to the team: Welcome back! Based on the sprint graph, here's your current status: - **Current Phase**: [phase from Sprint node] - **Current Stage**: [stage from Sprint node] +- **Phase Status**: [phase_status from Sprint node] - **Artifacts**: [count] Requirements, [count] User Stories, [count] Tasks, [count] Code Files - **Carried-Forward Context**: [count] artifacts from previous sprint (if any) - **Questions Asked**: [count] (view in Sprint page) diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/terminology.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/terminology.md index 98efc769..82043f3d 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/terminology.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/terminology.md @@ -49,7 +49,7 @@ The MCP tool used for ALL human communication. When called, it sends the questio ### Sprint -A Sprint is a graph node that represents a development cycle. It contains all artifacts (Requirements, UserStories, Tasks, CodeFiles, Questions, Reviews) via CONTAINS edges. The Sprint node's properties track the current `phase` and `current_stage`. +A Sprint is a graph node that represents a development cycle. It contains all artifacts (Requirements, UserStories, Tasks, CodeFiles, Questions, Reviews) via CONTAINS edges. The Sprint node is the single workflow cursor: `phase` is the UI-owned lifecycle phase, while `current_stage` and `phase_status` are agent-updated progress signals that the UI visualizes. ### Graph Nodes diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/welcome-message.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/welcome-message.md index de76b867..92f2906a 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/welcome-message.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/common/welcome-message.md @@ -88,7 +88,9 @@ AI-DLC is a structured yet flexible software development process that adapts to - The agent will **ask you questions** through the platform — you'll see them appear in the Sprint page - **Answer in the UI** and the agent will receive your response and continue working -- At each major checkpoint, the agent will **ask for your approval** before proceeding +- At each major checkpoint inside a phase, the agent will **ask for your approval** before proceeding +- Phase transitions are **your UI action**: when a phase is ready, the Sprint page shows the transition button and you choose when to move forward +- The Sprint page visualizes workflow state from the Sprint node's `phase`, `current_stage`, and `phase_status` properties - **All artifacts** (requirements, stories, tasks) are stored in the graph — view them anytime in the Sprint page - **All questions and answers** are automatically tracked for a complete audit trail @@ -98,8 +100,9 @@ AI-DLC is a structured yet flexible software development process that adapts to 2. **It gathers requirements** and asks clarifying questions via the platform 3. **It creates an execution plan** showing which stages to run and why 4. **You review and approve** the plan (or request changes) -5. **The agent executes the plan** with approval checkpoints at each major stage -6. **You get working code** with all artifacts tracked in the project graph +5. **The agent executes the plan** with approval checkpoints inside the active phase +6. **The agent marks the phase ready** and you move to the next phase from the Sprint page +7. **You get working code** with all artifacts tracked in the project graph The AI-DLC process adapts to: diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/build-and-test.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/build-and-test.md index 8ff419c9..6c3c9270 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/build-and-test.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/build-and-test.md @@ -64,7 +64,8 @@ Update Sprint and Task nodes: ``` update_node(label: "Sprint", id: env.sprintId, properties: { current_stage: "build-and-test", - phase: "CONSTRUCTION" + phase: "CONSTRUCTION", + phase_status: "ready_for_transition" }) ``` @@ -79,9 +80,9 @@ add_node(label: "Review", id: "review-build-test", properties: { --- -## Step 5: Request Approval +## Step 5: Finalize Construction Phase -Call `ask_question` with: +Present a final status summary in chat. Do **not** call `ask_question` for the phase transition. ``` "Build and Test Complete @@ -99,7 +100,7 @@ Generated instruction files: 3. integration-test-instructions.md 4. [additional files as needed] -Ready to proceed to Operations stage (placeholder) or complete the workflow?" +Construction phase is ready for human review. When the team is ready, create or inspect the PR, then click End Construction & Move to Review in the Sprint page." ``` -Wait for explicit approval. +Exit cleanly after the summary. If the team later requests changes, handle that as a new intra-phase refinement request and update the relevant Task or Review nodes. diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/code-generation.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/code-generation.md index a7f78989..51d876c4 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/code-generation.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/code-generation.md @@ -113,7 +113,8 @@ update_node(label: "Task", id: "unit-[name]", properties: { }) update_node(label: "Sprint", id: env.sprintId, properties: { - current_stage: "code-generation" + current_stage: "code-generation", + phase_status: "active" }) ``` diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/functional-design.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/functional-design.md index 34aa7cf9..9fcf3307 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/functional-design.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/functional-design.md @@ -74,7 +74,8 @@ add_node(label: "Requirement", id: "fd-[unit-name]-business-logic", properties: ``` update_node(label: "Sprint", id: env.sprintId, properties: { - current_stage: "functional-design" + current_stage: "functional-design", + phase_status: "active" }) ``` diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/infrastructure-design.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/infrastructure-design.md index 7a962779..a55b36e5 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/infrastructure-design.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/infrastructure-design.md @@ -47,7 +47,8 @@ add_node(label: "Requirement", id: "infra-design-[unit-name]", properties: { ``` update_node(label: "Sprint", id: env.sprintId, properties: { - current_stage: "infrastructure-design" + current_stage: "infrastructure-design", + phase_status: "active" }) ``` diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/nfr-design.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/nfr-design.md index 954e49d9..006c1028 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/nfr-design.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/nfr-design.md @@ -46,7 +46,8 @@ add_node(label: "Requirement", id: "nfr-design-[unit-name]", properties: { ``` update_node(label: "Sprint", id: env.sprintId, properties: { - current_stage: "nfr-design" + current_stage: "nfr-design", + phase_status: "active" }) ``` diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/nfr-requirements.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/nfr-requirements.md index 8777b588..14f77232 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/nfr-requirements.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/construction/nfr-requirements.md @@ -53,7 +53,8 @@ add_node(label: "Requirement", id: "nfr-[unit-name]-[area]", properties: { ``` update_node(label: "Sprint", id: env.sprintId, properties: { - current_stage: "nfr-requirements" + current_stage: "nfr-requirements", + phase_status: "active" }) ``` diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/application-design.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/application-design.md index 7da7cb0c..bb4719a5 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/application-design.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/application-design.md @@ -125,7 +125,8 @@ add_edge( ``` update_node(label: "Sprint", id: env.sprintId, properties: { - current_stage: "application-design" + current_stage: "application-design", + phase_status: "active" }) ``` diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/requirements-analysis.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/requirements-analysis.md index 6cff6895..3037c364 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/requirements-analysis.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/requirements-analysis.md @@ -155,7 +155,8 @@ add_edge( ``` update_node(label: "Sprint", id: env.sprintId, properties: { - current_stage: "requirements-analysis" + current_stage: "requirements-analysis", + phase_status: "active" }) ``` diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/reverse-engineering.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/reverse-engineering.md index 50dafae4..2a0b2e5d 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/reverse-engineering.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/reverse-engineering.md @@ -199,7 +199,8 @@ add_edge( ``` update_node(label: "Sprint", id: env.sprintId, properties: { - current_stage: "reverse-engineering" + current_stage: "reverse-engineering", + phase_status: "active" }) ``` diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/units-generation.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/units-generation.md index 627bda4a..11af68b2 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/units-generation.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/units-generation.md @@ -97,13 +97,14 @@ add_edge( ``` update_node(label: "Sprint", id: env.sprintId, properties: { - current_stage: "units-generation" + current_stage: "units-generation", + phase_status: "ready_for_transition" }) ``` -## Step 7: Request Approval +## Step 7: Finalize Inception Phase -Call `ask_question` with: +Present a final status summary in chat. Do **not** call `ask_question` for the phase transition. ``` "Units Generation Complete @@ -112,10 +113,10 @@ Call `ask_question` with: - [N] units created with dependencies mapped - All user stories assigned to units -Do you APPROVE to proceed to CONSTRUCTION PHASE, or describe what changes are needed?" +Inception phase is ready for human review. When the team is ready, click Approve & Move to Construction in the Sprint page." ``` -Wait for explicit approval. If changes requested, update Task nodes and re-request. +Exit cleanly after the summary. If the team later requests changes, handle that as a new intra-phase refinement request and update the relevant Task nodes. --- diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/user-stories.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/user-stories.md index c7935381..13b616ec 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/user-stories.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/user-stories.md @@ -160,7 +160,8 @@ Ensure stories follow INVEST criteria: ``` update_node(label: "Sprint", id: env.sprintId, properties: { - current_stage: "user-stories" + current_stage: "user-stories", + phase_status: "active" }) ``` diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/workflow-planning.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/workflow-planning.md index 8ccb6c14..fc9a494b 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/workflow-planning.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/workflow-planning.md @@ -69,6 +69,7 @@ Store the execution plan as Sprint node properties: ``` update_node(label: "Sprint", id: env.sprintId, properties: { current_stage: "workflow-planning", + phase_status: "active", execution_plan: "[JSON or text summary of stages to execute/skip with rationale]", risk_level: "[low/medium/high/critical]" }) diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/workspace-detection.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/workspace-detection.md index e9462df0..6233bdaf 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/workspace-detection.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rule-details/inception/workspace-detection.md @@ -93,6 +93,7 @@ Update the Sprint node to track current stage: update_node(label: "Sprint", id: env.sprintId, properties: { phase: "INCEPTION", current_stage: "workspace-detection", + phase_status: "active", project_type: "greenfield" or "brownfield", has_previous_context: "true" or "false" }) diff --git a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rules/core-workflow.md b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rules/core-workflow.md index 04899995..4801ce41 100644 --- a/lambda/agents-ecs/aidlc-rules/aws-aidlc-rules/core-workflow.md +++ b/lambda/agents-ecs/aidlc-rules/aws-aidlc-rules/core-workflow.md @@ -46,6 +46,8 @@ The AI model intelligently assesses what stages are needed based on: **NEVER create question files, use `[Answer]:` tags, or ask questions inline in chat. The `ask_question` tool is the ONLY way to get human input.** +**NO PHASE-BOUNDARY APPROVALS**: Do not use `ask_question` to ask whether the team approves moving from Inception to Construction, Construction to Review, or Review to Completed. Phase transitions are UI-owned. The agent updates Sprint workflow state and exits; the user clicks the UI transition button. + ## MANDATORY: Custom Welcome Message **CRITICAL**: When starting ANY software development request, you MUST display the welcome message. @@ -63,12 +65,22 @@ The AI model intelligently assesses what stages are needed based on: - Use `add_node` to create artifacts (Requirements, UserStories, Tasks, CodeFiles, Reviews). ALWAYS pass the `edges` parameter to link the new node to its parent in the same call (e.g. `edges: [{ direction: "from", label: "Requirement", id: "req-xxx", edgeLabel: "BREAKS_INTO" }]`). - Use `add_edge` only when you need to add a relationship after the fact (prefer using `edges` in `add_node` instead) -- Use `update_node` to track state changes (Sprint phase, stage, Task status) +- Use `update_node` to track workflow state on the Sprint node and work state on Task nodes - Use `get_sprint_graph` to load current state at the start of any stage - Use `list_nodes` to enumerate artifacts of a specific type - **NEVER** create markdown files as artifact output. The graph is the only output channel. - **Application code** (actual source files) is still written to the workspace filesystem. +## MANDATORY: Sprint Workflow State + +The Sprint node is the single source of truth for where the workflow is. The UI visualizes these properties directly. + +- `phase`: UI-owned lifecycle phase (`INCEPTION`, `CONSTRUCTION`, `REVIEW`, `COMPLETED`). Agents must not change this to transition phases. +- `current_stage`: agent-owned stage cursor in kebab-case, for example `requirements-analysis`, `units-generation`, or `build-and-test`. +- `phase_status`: agent-owned phase progress status. Use `active` while work is in progress and `ready_for_transition` when the final stage of a phase is complete and the UI should offer the phase-transition button. + +At the start of every stage, call `update_node(label: "Sprint", id: env.sprintId, properties: { current_stage: "[stage-id]", phase_status: "active" })`. At the end of terminal phase stages, update `phase_status` to `ready_for_transition` and provide a final summary without calling `ask_question` for the phase transition. + # Adaptive Software Development Workflow --- @@ -107,7 +119,7 @@ The AI model intelligently assesses what stages are needed based on: b. "Proceed directly" — continue with the carried-forward context loaded silently c. Or describe any changes since the last sprint that the agent should be aware of - **If no previous sprints**: Skip this step entirely -4. Update Sprint node with current stage: `update_node(label: "Sprint", id: env.sprintId, properties: {phase: "INCEPTION", current_stage: "workspace-detection"})` +4. Update Sprint node with current stage: `update_node(label: "Sprint", id: env.sprintId, properties: {phase: "INCEPTION", current_stage: "workspace-detection", phase_status: "active"})` 5. Determine next phase: Reverse Engineering (if brownfield and no current RE artifacts) OR Requirements Analysis 6. Present completion message to user (see workspace-detection.md for message formats) 7. Automatically proceed to next phase @@ -273,7 +285,8 @@ The AI model intelligently assesses what stages are needed based on: 2. Load reverse engineering artifacts from graph (if brownfield) 3. Execute at appropriate depth (minimal/standard/comprehensive) 4. Create Task nodes representing units of work via `add_node` -5. **Wait for Explicit Approval**: Call `ask_question` with approval prompt - DO NOT PROCEED until user confirms +5. Update Sprint workflow state to `current_stage: "units-generation"` and `phase_status: "ready_for_transition"` +6. Present a final Inception summary and exit. Do not call `ask_question` for the Inception → Construction transition. --- @@ -433,7 +446,8 @@ When all tasks done: 2. Generate comprehensive build and test instructions 3. Keep build/test instructions as filesystem files (these are actual working scripts/docs, not artifacts) 4. Update Task node statuses in graph as testing completes -5. **Wait for Explicit Approval**: Call `ask_question`: "Build and test instructions complete. Ready to proceed?" - DO NOT PROCEED until user confirms +5. Update Sprint workflow state to `current_stage: "build-and-test"` and `phase_status: "ready_for_transition"` +6. Present a final Construction summary and exit. Do not call `ask_question` for the Construction → Review transition. --- diff --git a/lambda/agents-ecs/mcp-server-graph/index.js b/lambda/agents-ecs/mcp-server-graph/index.js index d3a7b319..1ed7e806 100644 --- a/lambda/agents-ecs/mcp-server-graph/index.js +++ b/lambda/agents-ecs/mcp-server-graph/index.js @@ -342,7 +342,13 @@ dependency chain of the sprint at a glance.`, const vertices = await g .V() .has('Sprint', 'id', env.sprintId) - .union(__.out('CONTAINS'), __.out('HAS_REVIEW'), __.out('HAS_PR'), __.out('HAS_PR_GROUP')) + .union( + __.identity(), + __.out('CONTAINS'), + __.out('HAS_REVIEW'), + __.out('HAS_PR'), + __.out('HAS_PR_GROUP'), + ) .project('id', 'label', 'props') .by('id') .by(T_label) @@ -355,7 +361,13 @@ dependency chain of the sprint at a glance.`, const edges = await g .V() .has('Sprint', 'id', env.sprintId) - .union(__.out('CONTAINS'), __.out('HAS_REVIEW'), __.out('HAS_PR'), __.out('HAS_PR_GROUP')) + .union( + __.identity(), + __.out('CONTAINS'), + __.out('HAS_REVIEW'), + __.out('HAS_PR'), + __.out('HAS_PR_GROUP'), + ) .bothE() .where(__.otherV().has('id', P.within(...nodeIds))) .project('source', 'target', 'label') diff --git a/lambda/sprint-graph/index.js b/lambda/sprint-graph/index.js index 0c22133c..875d5710 100644 --- a/lambda/sprint-graph/index.js +++ b/lambda/sprint-graph/index.js @@ -39,17 +39,24 @@ export const handler = async (event) => { } const { sprintId } = event.pathParameters || {}; - // Get all vertices contained in this sprint (CONTAINS + HAS_REVIEW + HAS_PR + HAS_AGENT_RUN) + // Get the Sprint workflow cursor plus all vertices contained in this sprint. const vertices = await g .V() .has('Sprint', 'id', sprintId) - .union(__.out('CONTAINS'), __.out('HAS_REVIEW'), __.out('HAS_PR'), __.out('HAS_AGENT_RUN')) + .union( + __.identity(), + __.out('CONTAINS'), + __.out('HAS_REVIEW'), + __.out('HAS_PR'), + __.out('HAS_AGENT_RUN'), + ) .project('id', 'type', 'label', 'props') .by('id') .by(T.label) .by( __.coalesce( __.values('title'), + __.values('name'), __.values('file_path'), __.values('agent_type'), __.values('status'), @@ -65,7 +72,13 @@ export const handler = async (event) => { const edges = await g .V() .has('Sprint', 'id', sprintId) - .union(__.out('CONTAINS'), __.out('HAS_REVIEW'), __.out('HAS_PR'), __.out('HAS_AGENT_RUN')) + .union( + __.identity(), + __.out('CONTAINS'), + __.out('HAS_REVIEW'), + __.out('HAS_PR'), + __.out('HAS_AGENT_RUN'), + ) .bothE() .where(__.otherV().has('id', P.within(...nodeIds))) .project('source', 'target', 'label') diff --git a/lambda/sprint-graph/test/sprint-graph.test.js b/lambda/sprint-graph/test/sprint-graph.test.js index a9a3428e..6065b12c 100644 --- a/lambda/sprint-graph/test/sprint-graph.test.js +++ b/lambda/sprint-graph/test/sprint-graph.test.js @@ -36,7 +36,15 @@ beforeEach(async () => { await g.V().drop().next(); }); -const addSprint = async (sprintId) => g.addV('Sprint').property('id', sprintId).next(); +const addSprint = async (sprintId) => + g + .addV('Sprint') + .property('id', sprintId) + .property('name', 'Sprint') + .property('phase', 'INCEPTION') + .property('current_stage', 'requirements-analysis') + .property('phase_status', 'active') + .next(); // containment is one of: CONTAINS, HAS_REVIEW, HAS_PR, HAS_AGENT_RUN const addContained = async (sprintId, label, props, containment = 'CONTAINS') => { @@ -75,13 +83,25 @@ describe('OPTIONS', () => { }); describe('GET /sprint-graph', () => { - it('returns empty nodes and edges when the sprint has no contained vertices', async () => { + it('returns the Sprint workflow state when the sprint has no contained vertices', async () => { const sprintId = `s-${randomUUID()}`; await addSprint(sprintId); const res = await invoke(sprintId); expect(res.statusCode).toBe(200); - expect(JSON.parse(res.body)).toEqual({ nodes: [], edges: [] }); + expect(JSON.parse(res.body)).toEqual({ + nodes: [ + expect.objectContaining({ + id: sprintId, + type: 'Sprint', + label: 'Sprint', + phase: 'INCEPTION', + current_stage: 'requirements-analysis', + phase_status: 'active', + }), + ], + edges: [], + }); }); it('uses the title → file_path → agent_type → status → "(unnamed)" label fallback chain', async () => { @@ -108,6 +128,7 @@ describe('GET /sprint-graph', () => { const { nodes } = JSON.parse(res.body); const byId = Object.fromEntries(nodes.map((n) => [n.id, n])); + expect(byId[sprintId]).toMatchObject({ type: 'Sprint', label: 'Sprint' }); expect(byId[taskId]).toMatchObject({ type: 'Task', label: 'T' }); expect(byId[fileId]).toMatchObject({ type: 'CodeFile', label: '/a.js' }); expect(byId[runId]).toMatchObject({ type: 'AgentRun', label: 'inception' }); @@ -126,8 +147,8 @@ describe('GET /sprint-graph', () => { const res = await invoke(sprintId); const { nodes } = JSON.parse(res.body); - expect(nodes).toHaveLength(1); - expect(nodes[0]).toMatchObject({ + expect(nodes).toHaveLength(2); + expect(nodes.find((n) => n.id === taskId)).toMatchObject({ id: taskId, type: 'Task', label: 'My Task', @@ -161,7 +182,7 @@ describe('GET /sprint-graph', () => { const res = await invoke(sprintId); const { nodes, edges } = JSON.parse(res.body); - expect(nodes.map((n) => n.id)).toEqual([inSprint]); + expect(nodes.map((n) => n.id).sort()).toEqual([inSprint, sprintId].sort()); expect(edges).toEqual([]); }); @@ -190,7 +211,10 @@ describe('GET /sprint-graph', () => { // Same sprintId in our partition — should be empty. await addSprint(sprintId); const res = await invoke(sprintId); - expect(JSON.parse(res.body)).toEqual({ nodes: [], edges: [] }); + expect(JSON.parse(res.body)).toEqual({ + nodes: [expect.objectContaining({ id: sprintId, type: 'Sprint' })], + edges: [], + }); await otherG.V().drop().next(); }); diff --git a/lambda/sprints/index.js b/lambda/sprints/index.js index ffb27786..dca4578b 100644 --- a/lambda/sprints/index.js +++ b/lambda/sprints/index.js @@ -71,6 +71,8 @@ const mapSprint = (v) => { name: v.get('name')?.[0] || '', description: v.get('description')?.[0] || '', phase: v.get('phase')?.[0] || 'INCEPTION', + currentStage: v.get('current_stage')?.[0] || null, + phaseStatus: v.get('phase_status')?.[0] || null, createdAt: v.get('created_at')?.[0] || '', currentExecutionArn: arn && arn !== '' ? arn : null, currentExecutionId: execId && execId !== '' ? execId : null, @@ -180,6 +182,8 @@ export const handler = async (event) => { .property('name', data.name) .property('description', data.description || '') .property('phase', phase) + .property('current_stage', '') + .property('phase_status', 'active') .property('sprint_id', id) .property('created_at', createdAt) .property('current_execution_arn', '') @@ -205,6 +209,8 @@ export const handler = async (event) => { name: data.name, description: data.description || '', phase, + currentStage: null, + phaseStatus: 'active', createdAt, currentExecutionArn: null, currentExecutionId: null, @@ -231,9 +237,20 @@ export const handler = async (event) => { const existing = await g.V().has('Sprint', 'id', sprintId).valueMap().next(); if (!existing.value) return res(404, { error: 'Sprint not found' }); - // Agent state fields are system-write-only; strip them from user requests. - // The orchestrator and pool-worker update these via internal invocations. - const USER_WRITABLE = ['name', 'description', 'phase']; + // Execution fields are system-write-only; strip them from user requests. + // The UI may update phase/currentStage/phaseStatus when it performs a + // user-owned phase transition and persist selected construction + // branches, while agents update currentStage and phaseStatus directly + // on the Sprint vertex through graph tools. + const USER_WRITABLE = [ + 'name', + 'description', + 'phase', + 'currentStage', + 'phaseStatus', + 'branch', + 'baseBranch', + ]; const isSystemCaller = event.requestContext?.authorizer?.claims?.sub === 'system'; const SYSTEM_FIELDS = [ 'currentExecutionArn', @@ -279,6 +296,20 @@ export const handler = async (event) => { .property(cardinality.single, 'phase', data.phase) .next(); } + if (data.currentStage !== undefined) { + await g + .V() + .has('Sprint', 'id', sprintId) + .property(cardinality.single, 'current_stage', data.currentStage || '') + .next(); + } + if (data.phaseStatus !== undefined) { + await g + .V() + .has('Sprint', 'id', sprintId) + .property(cardinality.single, 'phase_status', data.phaseStatus || '') + .next(); + } // Agent state fields (system-caller only) if (data.currentExecutionArn !== undefined) { diff --git a/lambda/sprints/test/sprints.test.js b/lambda/sprints/test/sprints.test.js index a075f167..0b0d204e 100644 --- a/lambda/sprints/test/sprints.test.js +++ b/lambda/sprints/test/sprints.test.js @@ -101,6 +101,16 @@ const getSprint = async (sprintId) => { return JSON.parse(res.body); }; +const updateSprint = async (sprintId, body) => { + const res = await handler({ + httpMethod: 'PUT', + pathParameters: { sprintId }, + body: JSON.stringify(body), + }); + expect(res.statusCode).toBe(200); + return JSON.parse(res.body); +}; + describe('POST /sprints', () => { it('derives a github-issues tracker from issueNumber/issueUrl (legacy frontend path)', async () => { const projectId = await seedProject({ gitRepo: 'octo/repo' }); @@ -151,6 +161,8 @@ describe('POST /sprints', () => { const created = await createSprint(projectId, { name: 'plain' }); expect(created.tracker).toBeNull(); expect(created.issueNumber).toBeNull(); + expect(created.currentStage).toBeNull(); + expect(created.phaseStatus).toBe('active'); }); }); @@ -182,5 +194,32 @@ describe('GET /sprints/:id (backward compatibility)', () => { expect(fetched.tracker).toEqual(created.tracker); expect(fetched.issueNumber).toBe('7'); expect(fetched.issueUrl).toBe('https://github.com/octo/repo/issues/7'); + expect(fetched.currentStage).toBeNull(); + expect(fetched.phaseStatus).toBe('active'); + }); +}); + +describe('PUT /sprints/:id', () => { + it('allows the UI to own phase transitions and visible workflow state', async () => { + const projectId = await seedProject({ gitRepo: 'octo/repo' }); + const created = await createSprint(projectId, { name: 'S' }); + + const updated = await updateSprint(created.id, { + phase: 'CONSTRUCTION', + currentStage: 'code-generation', + phaseStatus: 'active', + branch: 'feature/sprint-1', + baseBranch: 'main', + currentAgentStatus: 'failed', + }); + + expect(updated).toMatchObject({ + phase: 'CONSTRUCTION', + currentStage: 'code-generation', + phaseStatus: 'active', + branch: 'feature/sprint-1', + baseBranch: 'main', + }); + expect(updated.currentAgentStatus).toBeNull(); }); }); From 9c12626d17d0a86030d1bdc1dac045f035e840c5 Mon Sep 17 00:00:00 2001 From: jwthewes Date: Wed, 10 Jun 2026 15:04:30 +0200 Subject: [PATCH 2/3] fix: preserve review phase transition --- frontend/src/pages/ConstructionPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/ConstructionPage.tsx b/frontend/src/pages/ConstructionPage.tsx index 7379a39f..b00fc61e 100644 --- a/frontend/src/pages/ConstructionPage.tsx +++ b/frontend/src/pages/ConstructionPage.tsx @@ -131,7 +131,7 @@ export default function ConstructionPage() { // workflow-state contract existed. If construction evidence already exists, // normalize the UI-owned phase so the dashboard no longer shows Inception as active. useEffect(() => { - if (!projectId || !sprintId || !sprint || sprint.phase === 'CONSTRUCTION') return; + if (!projectId || !sprintId || !sprint || sprint.phase !== 'INCEPTION') return; const hasConstructionEvidence = sprint.currentAgentType === 'construction-orchestrator' || sprint.currentAgentType === 'construction' || From 1e541ffd4afa988d87888c569e30f905cd3458cf Mon Sep 17 00:00:00 2001 From: jwthewes Date: Wed, 10 Jun 2026 15:31:36 +0200 Subject: [PATCH 3/3] fix: navigate on sprint phase changes --- frontend/src/components/layout/AppSidebar.tsx | 12 +++--------- frontend/src/lib/sprintPhaseNavigation.ts | 16 ++++++++++++++++ frontend/src/pages/ConstructionPage.tsx | 5 +++++ frontend/src/pages/InceptionPage.tsx | 5 +++++ frontend/src/pages/ReviewPage.tsx | 5 +++++ 5 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 frontend/src/lib/sprintPhaseNavigation.ts diff --git a/frontend/src/components/layout/AppSidebar.tsx b/frontend/src/components/layout/AppSidebar.tsx index 9a8cc422..b68a44ca 100644 --- a/frontend/src/components/layout/AppSidebar.tsx +++ b/frontend/src/components/layout/AppSidebar.tsx @@ -6,12 +6,7 @@ import { PipelineView } from '@/components/layout/PipelineView'; import { useState, useEffect, useCallback } from 'react'; import { sprintsService, type Sprint } from '@/services/sprints'; import { realtimeService } from '@/services/realtime'; - -const PHASE_URL_SUFFIX: Record = { - INCEPTION: '', - CONSTRUCTION: '/construction', - REVIEW: '/review', -}; +import { getSprintPhasePath } from '@/lib/sprintPhaseNavigation'; export function AppSidebar() { const navigate = useNavigate(); @@ -45,9 +40,8 @@ export function AppSidebar() { realtimeService.on('agent.error', () => loadSprint()), realtimeService.on('sprint.phaseChanged', (data: { phase?: string }) => { loadSprint(); - if (data.phase && PHASE_URL_SUFFIX[data.phase] !== undefined) { - navigate(`/project/${projectId}/sprint/${sprintId}${PHASE_URL_SUFFIX[data.phase]}`); - } + const nextPath = data.phase ? getSprintPhasePath(projectId, sprintId, data.phase) : null; + if (nextPath) navigate(nextPath); }), ]; return () => unsubs.forEach((unsub) => unsub()); diff --git a/frontend/src/lib/sprintPhaseNavigation.ts b/frontend/src/lib/sprintPhaseNavigation.ts new file mode 100644 index 00000000..9885797e --- /dev/null +++ b/frontend/src/lib/sprintPhaseNavigation.ts @@ -0,0 +1,16 @@ +const PHASE_URL_SUFFIX: Record = { + INCEPTION: '', + CONSTRUCTION: '/construction', + REVIEW: '/review', + COMPLETED: '/review', +}; + +export function getSprintPhasePath( + projectId: string, + sprintId: string, + phase: string, +): string | null { + const suffix = PHASE_URL_SUFFIX[phase]; + if (suffix === undefined) return null; + return `/project/${projectId}/sprint/${sprintId}${suffix}`; +} diff --git a/frontend/src/pages/ConstructionPage.tsx b/frontend/src/pages/ConstructionPage.tsx index b00fc61e..e6781f9a 100644 --- a/frontend/src/pages/ConstructionPage.tsx +++ b/frontend/src/pages/ConstructionPage.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useSprint } from '@/contexts/SprintContext'; import { useAuth } from '@/contexts/AuthContext'; import { usePresence } from '@/hooks/usePresence'; @@ -59,8 +60,10 @@ import type { StructuredAnswer } from '@/services/questions'; import { TaskSettingsDialog } from '@/components/settings/TaskSettingsDialog'; import { TaskActionsMenu } from '@/components/domain/TaskActionsMenu'; import type { Task } from '@/services/tasks'; +import { getSprintPhasePath } from '@/lib/sprintPhaseNavigation'; export default function ConstructionPage() { + const navigate = useNavigate(); const { user } = useAuth(); const { sprint, tasks, codeFiles, questions, projectId, sprintId, reload } = useSprint(); @@ -338,6 +341,8 @@ export default function ConstructionPage() { .create(sprintId, { type: 'phase_changed', title: 'Moved to Review phase', userName }) .catch(() => {}); await reload(); + const nextPath = getSprintPhasePath(projectId, sprintId, 'REVIEW'); + if (nextPath) navigate(nextPath); } catch (err) { console.error('Failed to approve phase:', err); } finally { diff --git a/frontend/src/pages/InceptionPage.tsx b/frontend/src/pages/InceptionPage.tsx index 42e40676..7a1d75cb 100644 --- a/frontend/src/pages/InceptionPage.tsx +++ b/frontend/src/pages/InceptionPage.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useSprint } from '@/contexts/SprintContext'; import { useAuth } from '@/contexts/AuthContext'; import { usePresence } from '@/hooks/usePresence'; @@ -50,6 +51,7 @@ import { CollaborativeTextarea } from '@/components/CollaborativeTextarea'; import { AiModifyModal } from '@/components/AiModifyModal'; import { AgentStartErrorBanner } from '@/components/AgentStartErrorBanner'; import { extractAgentStartError, type AgentStartError } from '@/lib/agentStartError'; +import { getSprintPhasePath } from '@/lib/sprintPhaseNavigation'; import { AlertCircle, ArrowRight, @@ -68,6 +70,7 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; export default function InceptionPage() { + const navigate = useNavigate(); const { user } = useAuth(); const { sprint, @@ -302,6 +305,8 @@ export default function InceptionPage() { }) .catch(() => {}); await reload(); + const nextPath = getSprintPhasePath(projectId, sprintId, 'CONSTRUCTION'); + if (nextPath) navigate(nextPath); } catch (err) { console.error('Start over failed:', err); } diff --git a/frontend/src/pages/ReviewPage.tsx b/frontend/src/pages/ReviewPage.tsx index a1432be7..47917693 100644 --- a/frontend/src/pages/ReviewPage.tsx +++ b/frontend/src/pages/ReviewPage.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useSprint } from '@/contexts/SprintContext'; import { useAuth } from '@/contexts/AuthContext'; import { usePresence } from '@/hooks/usePresence'; @@ -35,6 +36,7 @@ import ReviewEditor from '@/components/ReviewEditor'; import CodeFileViewer from '@/components/CodeFileViewer'; import { BranchSelector } from '@/components/BranchSelector'; import { PrCheckoutCommand } from '@/components/PrCheckoutCommand'; +import { getSprintPhasePath } from '@/lib/sprintPhaseNavigation'; import { AgentStartErrorBanner } from '@/components/AgentStartErrorBanner'; import { Play, @@ -130,6 +132,7 @@ function ReviewStatusBar({ } export default function ReviewPage() { + const navigate = useNavigate(); const { user } = useAuth(); const { sprint, @@ -957,6 +960,8 @@ export default function ReviewPage() { data: { phase: 'COMPLETED', sprintId }, }); await reload(); + const nextPath = getSprintPhasePath(projectId, sprintId, 'COMPLETED'); + if (nextPath) navigate(nextPath); } }} />