diff --git a/apis/json-schema/Feature.yaml b/apis/json-schema/Feature.yaml index 86a568ce3..5ba2a2f0d 100644 --- a/apis/json-schema/Feature.yaml +++ b/apis/json-schema/Feature.yaml @@ -50,10 +50,10 @@ properties: repositoryId: $ref: UUID.yaml description: ID of the Repository entity this feature belongs to - fast: - type: boolean - default: false - description: When true, SDLC phases were skipped and the feature was implemented directly from the prompt + mode: + $ref: FeatureMode.yaml + default: Regular + description: "Execution mode determining the workflow: Regular (full SDLC), Fast (direct implementation), or Exploration (iterative prototyping)" push: type: boolean default: false @@ -92,6 +92,17 @@ properties: worktreePath: type: string description: Absolute path to the git worktree for this feature + iterationCount: + type: integer + minimum: -2147483648 + maximum: 2147483647 + default: 0 + description: Current feedback iteration count in exploration mode (0 when not exploring) + maxIterations: + type: integer + minimum: -2147483648 + maximum: 2147483647 + description: Maximum allowed iterations for exploration mode (only set when mode is Exploration) pr: $ref: PullRequest.yaml description: Pull request data (null until PR created) @@ -116,7 +127,7 @@ required: - lifecycle - messages - relatedArtifacts - - fast + - mode - push - openPr - forkAndPr @@ -126,6 +137,7 @@ required: - injectSkills - commitEvidence - approvalGates + - iterationCount allOf: - $ref: SoftDeletableEntity.yaml description: Central entity tracking a piece of work through the SDLC lifecycle (Aggregate Root) diff --git a/apis/json-schema/FeatureMode.yaml b/apis/json-schema/FeatureMode.yaml new file mode 100644 index 000000000..7037329bf --- /dev/null +++ b/apis/json-schema/FeatureMode.yaml @@ -0,0 +1,8 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: FeatureMode.yaml +type: string +enum: + - Regular + - Fast + - Exploration +description: Execution mode determining the feature's workflow and agent graph diff --git a/apis/json-schema/SdlcLifecycle.yaml b/apis/json-schema/SdlcLifecycle.yaml index 081660df5..b1431bf6d 100644 --- a/apis/json-schema/SdlcLifecycle.yaml +++ b/apis/json-schema/SdlcLifecycle.yaml @@ -12,6 +12,7 @@ enum: - Maintain - Blocked - Pending + - Exploring - Deleting - AwaitingUpstream - Archived diff --git a/apis/json-schema/WorkflowConfig.yaml b/apis/json-schema/WorkflowConfig.yaml index 17f510e06..7f4a4e2c4 100644 --- a/apis/json-schema/WorkflowConfig.yaml +++ b/apis/json-schema/WorkflowConfig.yaml @@ -55,10 +55,15 @@ properties: hideCiStatus: type: boolean description: "Hide CI status badges from UI (default: true)" - defaultFastMode: - type: boolean - default: true - description: "Default new features to fast mode (default: true)" + defaultMode: + type: string + default: Fast + description: "Default feature mode for new features: 'Regular', 'Fast', or 'Exploration' (default: 'Fast')" + explorationMaxIterations: + type: integer + minimum: -2147483648 + maximum: 2147483647 + description: "Maximum exploration feedback iterations (default: 10, 0 = unlimited)" autoArchiveDelayMinutes: type: integer minimum: -2147483648 @@ -73,5 +78,5 @@ required: - ciWatchEnabled - enableEvidence - commitEvidence - - defaultFastMode + - defaultMode description: Global workflow configuration defaults diff --git a/packages/core/src/application/ports/output/agents/feature-agent-process.interface.ts b/packages/core/src/application/ports/output/agents/feature-agent-process.interface.ts index de747e9e3..16e01fb0c 100644 --- a/packages/core/src/application/ports/output/agents/feature-agent-process.interface.ts +++ b/packages/core/src/application/ports/output/agents/feature-agent-process.interface.ts @@ -10,7 +10,7 @@ * - Infrastructure layer provides concrete implementation */ -import type { ApprovalGates, AgentType } from '../../../../domain/generated/output.js'; +import type { ApprovalGates, AgentType, FeatureMode } from '../../../../domain/generated/output.js'; /** * Service interface for feature agent background process management. @@ -45,7 +45,7 @@ export interface IFeatureAgentProcessService { commitEvidence?: boolean; resumePayload?: string; agentType?: AgentType; - fast?: boolean; + mode?: FeatureMode; model?: string; resumeReason?: string; } diff --git a/packages/core/src/application/ports/output/services/spec-initializer.interface.ts b/packages/core/src/application/ports/output/services/spec-initializer.interface.ts index 238749876..bc7faab35 100644 --- a/packages/core/src/application/ports/output/services/spec-initializer.interface.ts +++ b/packages/core/src/application/ports/output/services/spec-initializer.interface.ts @@ -24,13 +24,14 @@ export interface ISpecInitializerService { * - tasks.yaml * - feature.yaml * - * When mode is 'fast', only feature.yaml is created (no spec/research/plan/tasks). + * When mode is 'fast', only feature.yaml and spec.yaml are created. + * When mode is 'exploration', only feature.yaml is created. * * @param basePath - Directory to create specs/ in (typically the worktree path) * @param slug - Feature slug (kebab-case, e.g., "user-authentication") * @param featureNumber - Sequential feature number (will be zero-padded to 3 digits) * @param description - Feature description for template substitution - * @param mode - Optional mode; when 'fast', only feature.yaml is created + * @param mode - Optional mode; controls which template files are created * @returns The spec directory path and feature number used */ initialize( @@ -38,6 +39,6 @@ export interface ISpecInitializerService { slug: string, featureNumber: number, description: string, - mode?: 'fast' + mode?: 'fast' | 'exploration' ): Promise; } diff --git a/packages/core/src/application/use-cases/agents/approve-agent-run.use-case.ts b/packages/core/src/application/use-cases/agents/approve-agent-run.use-case.ts index 048297c0c..90490a7d5 100644 --- a/packages/core/src/application/use-cases/agents/approve-agent-run.use-case.ts +++ b/packages/core/src/application/use-cases/agents/approve-agent-run.use-case.ts @@ -15,7 +15,7 @@ import type { IAgentRunRepository } from '../../ports/output/agents/agent-run-re import type { IFeatureAgentProcessService } from '../../ports/output/agents/feature-agent-process.interface.js'; import type { IPhaseTimingRepository } from '../../ports/output/agents/phase-timing-repository.interface.js'; import type { IFeatureRepository } from '../../ports/output/repositories/feature-repository.interface.js'; -import { AgentRunStatus } from '../../../domain/generated/output.js'; +import { AgentRunStatus, FeatureMode } from '../../../domain/generated/output.js'; import type { PrdApprovalPayload } from '../../../domain/generated/output.js'; import { writeSpecFileAtomic, @@ -139,7 +139,7 @@ export class ApproveAgentRunUseCase { ...(payload ? { resumePayload: JSON.stringify(payload) } : {}), agentType: run.agentType, ...(run.modelId ? { model: run.modelId } : {}), - ...(feature?.fast ? { fast: true } : {}), + ...(feature?.mode && feature.mode !== FeatureMode.Regular ? { mode: feature.mode } : {}), } ); diff --git a/packages/core/src/application/use-cases/agents/reject-agent-run.use-case.ts b/packages/core/src/application/use-cases/agents/reject-agent-run.use-case.ts index 3e398b341..5bec5c7f2 100644 --- a/packages/core/src/application/use-cases/agents/reject-agent-run.use-case.ts +++ b/packages/core/src/application/use-cases/agents/reject-agent-run.use-case.ts @@ -14,7 +14,7 @@ import type { IAgentRunRepository } from '../../ports/output/agents/agent-run-re import type { IFeatureAgentProcessService } from '../../ports/output/agents/feature-agent-process.interface.js'; import type { IPhaseTimingRepository } from '../../ports/output/agents/phase-timing-repository.interface.js'; import type { IFeatureRepository } from '../../ports/output/repositories/feature-repository.interface.js'; -import { AgentRunStatus } from '../../../domain/generated/output.js'; +import { AgentRunStatus, FeatureMode } from '../../../domain/generated/output.js'; import type { PrdRejectionPayload, RejectionFeedbackEntry, @@ -167,7 +167,7 @@ export class RejectAgentRunUseCase { resumePayload: JSON.stringify(rejectionPayload), agentType: run.agentType, ...(run.modelId ? { model: run.modelId } : {}), - ...(feature.fast ? { fast: true } : {}), + ...(feature.mode !== FeatureMode.Regular ? { mode: feature.mode } : {}), } ); diff --git a/packages/core/src/application/use-cases/features/adopt-branch.use-case.ts b/packages/core/src/application/use-cases/features/adopt-branch.use-case.ts index 3ea0fdfdd..5adc93a24 100644 --- a/packages/core/src/application/use-cases/features/adopt-branch.use-case.ts +++ b/packages/core/src/application/use-cases/features/adopt-branch.use-case.ts @@ -13,7 +13,7 @@ import { injectable, inject } from 'tsyringe'; import { randomUUID } from 'node:crypto'; import type { Feature, PullRequest } from '../../../domain/generated/output.js'; -import { SdlcLifecycle, PrStatus } from '../../../domain/generated/output.js'; +import { SdlcLifecycle, PrStatus, FeatureMode } from '../../../domain/generated/output.js'; import type { IFeatureRepository } from '../../ports/output/repositories/feature-repository.interface.js'; import type { IRepositoryRepository } from '../../ports/output/repositories/repository-repository.interface.js'; import type { IWorktreeService } from '../../ports/output/services/worktree-service.interface.js'; @@ -114,7 +114,8 @@ export class AdoptBranchUseCase { lifecycle, messages: [], relatedArtifacts: [], - fast: false, + mode: FeatureMode.Regular, + iterationCount: 0, push: false, openPr: hasOpenPr, forkAndPr: false, diff --git a/packages/core/src/application/use-cases/features/check-and-unblock-features.use-case.ts b/packages/core/src/application/use-cases/features/check-and-unblock-features.use-case.ts index e8f4f1104..903919b37 100644 --- a/packages/core/src/application/use-cases/features/check-and-unblock-features.use-case.ts +++ b/packages/core/src/application/use-cases/features/check-and-unblock-features.use-case.ts @@ -16,7 +16,7 @@ */ import { injectable, inject } from 'tsyringe'; -import { SdlcLifecycle } from '../../../domain/generated/output.js'; +import { SdlcLifecycle, FeatureMode } from '../../../domain/generated/output.js'; import type { IFeatureRepository } from '../../ports/output/repositories/feature-repository.interface.js'; import type { IFeatureAgentProcessService } from '../../ports/output/agents/feature-agent-process.interface.js'; import { POST_IMPLEMENTATION } from '../../../domain/lifecycle-gates.js'; @@ -72,7 +72,7 @@ export class CheckAndUnblockFeaturesUseCase { ciWatchEnabled: child.ciWatchEnabled, enableEvidence: child.enableEvidence, commitEvidence: child.commitEvidence, - ...(child.fast ? { fast: true } : {}), + ...(child.mode !== FeatureMode.Regular ? { mode: child.mode } : {}), } ); } diff --git a/packages/core/src/application/use-cases/features/create/create-feature.use-case.ts b/packages/core/src/application/use-cases/features/create/create-feature.use-case.ts index 5c7813d8b..36f9f2aa7 100644 --- a/packages/core/src/application/use-cases/features/create/create-feature.use-case.ts +++ b/packages/core/src/application/use-cases/features/create/create-feature.use-case.ts @@ -22,6 +22,7 @@ import type { Feature } from '../../../../domain/generated/output.js'; import { SdlcLifecycle, AgentRunStatus, + FeatureMode, type AgentType, } from '../../../../domain/generated/output.js'; import type { IFeatureRepository } from '../../../ports/output/repositories/feature-repository.interface.js'; @@ -86,9 +87,13 @@ export class CreateFeatureUseCase { * No AI calls, no git operations — just DB writes. */ async createRecord(input: CreateFeatureInput): Promise { - let initialLifecycle: SdlcLifecycle = input.fast - ? SdlcLifecycle.Implementation - : SdlcLifecycle.Requirements; + const effectiveMode = input.mode ?? FeatureMode.Regular; + let initialLifecycle: SdlcLifecycle = + effectiveMode === FeatureMode.Exploration + ? SdlcLifecycle.Exploring + : effectiveMode === FeatureMode.Fast + ? SdlcLifecycle.Implementation + : SdlcLifecycle.Requirements; let shouldSpawn = true; let effectiveRepoPath = input.repositoryPath.replace(/\\/g, '/'); @@ -173,7 +178,7 @@ export class CreateFeatureUseCase { lifecycle: initialLifecycle, messages: [], relatedArtifacts: [], - fast: input.fast ?? false, + mode: effectiveMode, push: input.push ?? false, openPr: input.openPr ?? false, forkAndPr: input.forkAndPr ?? false, @@ -187,6 +192,10 @@ export class CreateFeatureUseCase { allowPlan: false, allowMerge: false, }, + iterationCount: 0, + ...(effectiveMode === FeatureMode.Exploration && { + maxIterations: getSettings().workflow.explorationMaxIterations ?? 10, + }), agentRunId: runId, specPath: '', repositoryId: repository.id, @@ -278,7 +287,11 @@ export class CreateFeatureUseCase { slug, featureNumber, input.userInput, - input.fast ? 'fast' : undefined + feature.mode === FeatureMode.Fast + ? 'fast' + : feature.mode === FeatureMode.Exploration + ? 'exploration' + : undefined ); // Commit pending attachments if sessionId was provided (web UI flow) @@ -387,7 +400,7 @@ export class CreateFeatureUseCase { ciWatchEnabled: input.ciWatchEnabled ?? true, enableEvidence: input.enableEvidence ?? false, commitEvidence: input.commitEvidence ?? false, - ...(input.fast ? { fast: true } : {}), + ...(feature.mode !== FeatureMode.Regular ? { mode: feature.mode } : {}), ...(input.agentType ? { agentType: input.agentType as AgentType } : {}), ...(input.model ? { model: input.model } : {}), } diff --git a/packages/core/src/application/use-cases/features/create/types.ts b/packages/core/src/application/use-cases/features/create/types.ts index 7dac5c14a..d6f7a96e4 100644 --- a/packages/core/src/application/use-cases/features/create/types.ts +++ b/packages/core/src/application/use-cases/features/create/types.ts @@ -1,4 +1,5 @@ import type { ApprovalGates, Attachment, Feature } from '../../../../domain/generated/output.js'; +import { type FeatureMode } from '../../../../domain/generated/output.js'; export interface CreateFeatureInput { userInput: string; @@ -12,8 +13,8 @@ export interface CreateFeatureInput { name?: string; /** Pre-supplied description (skips AI metadata extraction for description). */ description?: string; - /** When true, skip SDLC phases and implement directly from the user prompt. */ - fast?: boolean; + /** Execution mode: Regular (full SDLC), Fast (direct implementation), or Exploration (iterative prototyping). */ + mode?: FeatureMode; /** Fork repo and create PR to upstream at merge time (default: false). */ forkAndPr?: boolean; /** Commit specs/evidences into the repo (default: true, auto-false when forkAndPr). */ diff --git a/packages/core/src/application/use-cases/features/delete-feature.use-case.ts b/packages/core/src/application/use-cases/features/delete-feature.use-case.ts index 23b834f20..7a4ec7c89 100644 --- a/packages/core/src/application/use-cases/features/delete-feature.use-case.ts +++ b/packages/core/src/application/use-cases/features/delete-feature.use-case.ts @@ -14,6 +14,9 @@ */ import { injectable, inject } from 'tsyringe'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import { unlink } from 'node:fs/promises'; import type { Feature } from '../../../domain/generated/output.js'; import { AgentRunStatus, PrStatus, SdlcLifecycle } from '../../../domain/generated/output.js'; import type { IFeatureRepository } from '../../ports/output/repositories/feature-repository.interface.js'; @@ -137,6 +140,16 @@ export class DeleteFeatureUseCase { } await this.runRepo.updateStatus(run.id, AgentRunStatus.cancelled); } + + // Clean up checkpoint database file (used by LangGraph for state persistence) + if (run?.threadId) { + const checkpointPath = join(homedir(), '.shep', 'checkpoints', `${run.threadId}.db`); + try { + await unlink(checkpointPath); + } catch { + // Checkpoint file may not exist or already be removed + } + } } // Cleanup worktree and branches directly using the feature data we already diff --git a/packages/core/src/application/use-cases/features/promote/promote-exploration.use-case.ts b/packages/core/src/application/use-cases/features/promote/promote-exploration.use-case.ts new file mode 100644 index 000000000..366cf8aed --- /dev/null +++ b/packages/core/src/application/use-cases/features/promote/promote-exploration.use-case.ts @@ -0,0 +1,151 @@ +/** + * Promote Exploration Use Case + * + * Transitions an exploration feature to Regular or Fast mode via in-place + * mode transition. Changes the mode field, transitions lifecycle from + * Exploring to Requirements (regular) or Implementation (fast), optionally + * scaffolds missing spec YAMLs when promoting to regular, and spawns the + * appropriate agent graph. + * + * Business Rules: + * - Feature must be in Exploration mode and Exploring lifecycle + * - Promotion preserves existing worktree and branch (prototype code) + * - Promoting to Regular scaffolds missing spec YAMLs (spec, research, plan, tasks) + * - Promoting to Fast does not scaffold spec YAMLs + * - Spawns the appropriate agent graph after transition + */ + +import { injectable, inject } from 'tsyringe'; +import { randomUUID } from 'node:crypto'; +import type { Feature } from '../../../../domain/generated/output.js'; +import { SdlcLifecycle, FeatureMode, AgentRunStatus } from '../../../../domain/generated/output.js'; +import { EXPLORING_TRANSITIONS } from '../../../../domain/lifecycle-gates.js'; +import type { IFeatureRepository } from '../../../ports/output/repositories/feature-repository.interface.js'; +import type { IFeatureAgentProcessService } from '../../../ports/output/agents/feature-agent-process.interface.js'; +import type { IAgentRunRepository } from '../../../ports/output/agents/agent-run-repository.interface.js'; +import type { ISpecInitializerService } from '../../../ports/output/services/spec-initializer.interface.js'; +import type { IWorktreeService } from '../../../ports/output/services/worktree-service.interface.js'; +import { getSettings } from '../../../../infrastructure/services/settings.service.js'; + +export interface PromoteExplorationInput { + featureId: string; + targetMode: FeatureMode.Regular | FeatureMode.Fast; +} + +export interface PromoteExplorationResult { + feature: Feature; +} + +@injectable() +export class PromoteExplorationUseCase { + constructor( + @inject('IFeatureRepository') + private readonly featureRepo: IFeatureRepository, + @inject('IFeatureAgentProcessService') + private readonly processService: IFeatureAgentProcessService, + @inject('IAgentRunRepository') + private readonly runRepo: IAgentRunRepository, + @inject('ISpecInitializerService') + private readonly specInitializer: ISpecInitializerService, + @inject('IWorktreeService') + private readonly worktreeService: IWorktreeService + ) {} + + async execute(input: PromoteExplorationInput): Promise { + const feature = + (await this.featureRepo.findById(input.featureId)) ?? + (await this.featureRepo.findByIdPrefix(input.featureId)); + if (!feature) { + throw new Error(`Feature not found: ${input.featureId}`); + } + + if (feature.mode !== FeatureMode.Exploration) { + throw new Error( + `Feature "${feature.name}" is not in Exploration mode (current: ${feature.mode}). Only exploration features can be promoted.` + ); + } + + if (feature.lifecycle !== SdlcLifecycle.Exploring) { + throw new Error( + `Feature "${feature.name}" is not in Exploring lifecycle (current: ${feature.lifecycle}). Only features in Exploring state can be promoted.` + ); + } + + const targetLifecycle = + input.targetMode === FeatureMode.Fast + ? SdlcLifecycle.Implementation + : SdlcLifecycle.Requirements; + + // Validate the transition is allowed + if (!EXPLORING_TRANSITIONS.has(targetLifecycle)) { + throw new Error(`Invalid promotion target lifecycle: ${targetLifecycle}`); + } + + // Scaffold missing spec YAMLs when promoting to regular mode + if (input.targetMode === FeatureMode.Regular && feature.specPath) { + const worktreePath = + feature.worktreePath ?? + this.worktreeService.getWorktreePath(feature.repositoryPath, feature.branch); + await this.specInitializer.initialize( + worktreePath, + feature.slug, + 0, // Feature number hint — resolveNextNumber will use existing dir + feature.userQuery + ); + } + + // Create a new agent run for the promoted mode + const settings = getSettings(); + const runId = randomUUID(); + const agentRun = { + id: runId, + agentType: settings.agent.type, + agentName: 'feature-agent', + status: AgentRunStatus.pending, + prompt: feature.userQuery, + threadId: randomUUID(), + featureId: feature.id, + repositoryPath: feature.repositoryPath, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + await this.runRepo.create(agentRun); + + // Update the feature: mode, lifecycle, and agent run reference + const updatedFeature: Feature = { + ...feature, + mode: input.targetMode, + lifecycle: targetLifecycle, + agentRunId: runId, + updatedAt: new Date(), + }; + await this.featureRepo.update(updatedFeature); + + // Spawn the appropriate agent graph + const worktreePath = + feature.worktreePath ?? + this.worktreeService.getWorktreePath(feature.repositoryPath, feature.branch); + + this.processService.spawn( + feature.id, + runId, + feature.repositoryPath, + feature.specPath ?? '', + worktreePath, + { + approvalGates: feature.approvalGates, + threadId: agentRun.threadId, + push: feature.push, + openPr: feature.openPr, + forkAndPr: feature.forkAndPr, + commitSpecs: feature.commitSpecs, + ciWatchEnabled: feature.ciWatchEnabled, + enableEvidence: feature.enableEvidence, + commitEvidence: feature.commitEvidence, + ...(input.targetMode !== FeatureMode.Regular ? { mode: input.targetMode } : {}), + } + ); + + return { feature: updatedFeature }; + } +} diff --git a/packages/core/src/application/use-cases/features/resume-feature.use-case.ts b/packages/core/src/application/use-cases/features/resume-feature.use-case.ts index abb33252a..e247d5de7 100644 --- a/packages/core/src/application/use-cases/features/resume-feature.use-case.ts +++ b/packages/core/src/application/use-cases/features/resume-feature.use-case.ts @@ -8,7 +8,7 @@ import { injectable, inject } from 'tsyringe'; import { randomUUID } from 'node:crypto'; import type { Feature, AgentRun } from '../../../domain/generated/output.js'; -import { AgentRunStatus } from '../../../domain/generated/output.js'; +import { AgentRunStatus, FeatureMode } from '../../../domain/generated/output.js'; import type { IFeatureRepository } from '../../ports/output/repositories/feature-repository.interface.js'; import type { IAgentRunRepository } from '../../ports/output/agents/agent-run-repository.interface.js'; import type { IFeatureAgentProcessService } from '../../ports/output/agents/feature-agent-process.interface.js'; @@ -141,7 +141,7 @@ export class ResumeFeatureUseCase { enableEvidence: feature.enableEvidence, commitEvidence: feature.commitEvidence, agentType: lastRun.agentType, - ...(feature.fast ? { fast: true } : {}), + ...(feature.mode !== FeatureMode.Regular ? { mode: feature.mode } : {}), ...(lastRun.modelId ? { model: lastRun.modelId } : {}), resumeReason: lastRun.status, } diff --git a/packages/core/src/application/use-cases/features/start-feature.use-case.ts b/packages/core/src/application/use-cases/features/start-feature.use-case.ts index 0cecce648..5d0ec0d49 100644 --- a/packages/core/src/application/use-cases/features/start-feature.use-case.ts +++ b/packages/core/src/application/use-cases/features/start-feature.use-case.ts @@ -8,7 +8,7 @@ import { injectable, inject } from 'tsyringe'; import type { Feature, AgentRun } from '../../../domain/generated/output.js'; -import { SdlcLifecycle } from '../../../domain/generated/output.js'; +import { SdlcLifecycle, FeatureMode } from '../../../domain/generated/output.js'; import type { IFeatureRepository } from '../../ports/output/repositories/feature-repository.interface.js'; import type { IAgentRunRepository } from '../../ports/output/agents/agent-run-repository.interface.js'; import type { IFeatureAgentProcessService } from '../../ports/output/agents/feature-agent-process.interface.js'; @@ -83,7 +83,10 @@ export class StartFeatureUseCase { } // Check parent gate if feature has a parent - let targetLifecycle = resolved.fast ? SdlcLifecycle.Implementation : SdlcLifecycle.Requirements; + let targetLifecycle = + resolved.mode === FeatureMode.Fast + ? SdlcLifecycle.Implementation + : SdlcLifecycle.Requirements; let shouldSpawn = true; if (resolved.parentId) { @@ -130,7 +133,7 @@ export class StartFeatureUseCase { enableEvidence: resolved.enableEvidence, commitEvidence: resolved.commitEvidence, agentType: agentRun.agentType, - ...(resolved.fast ? { fast: true } : {}), + ...(resolved.mode !== FeatureMode.Regular ? { mode: resolved.mode } : {}), ...(agentRun.modelId ? { model: agentRun.modelId } : {}), } ); diff --git a/packages/core/src/domain/factories/settings-defaults.factory.ts b/packages/core/src/domain/factories/settings-defaults.factory.ts index 47c51713f..26b326daa 100644 --- a/packages/core/src/domain/factories/settings-defaults.factory.ts +++ b/packages/core/src/domain/factories/settings-defaults.factory.ts @@ -208,7 +208,8 @@ export function createDefaultSettings(): Settings { ciWatchEnabled: true, enableEvidence: false, commitEvidence: false, - defaultFastMode: true, + defaultMode: 'Fast', + explorationMaxIterations: 10, autoArchiveDelayMinutes: 10, skillInjection, }; diff --git a/packages/core/src/domain/generated/output.ts b/packages/core/src/domain/generated/output.ts index 9bc454c49..19c76dd45 100644 --- a/packages/core/src/domain/generated/output.ts +++ b/packages/core/src/domain/generated/output.ts @@ -495,9 +495,13 @@ export type WorkflowConfig = { */ hideCiStatus?: boolean; /** - * Default new features to fast mode (default: true) + * Default feature mode for new features: 'Regular', 'Fast', or 'Exploration' (default: 'Fast') */ - defaultFastMode: boolean; + defaultMode: string; + /** + * Maximum exploration feedback iterations (default: 10, 0 = unlimited) + */ + explorationMaxIterations?: number; /** * Minutes after completion before auto-archiving a feature (default: 10, 0 = disabled) */ @@ -884,10 +888,16 @@ export enum SdlcLifecycle { Maintain = 'Maintain', Blocked = 'Blocked', Pending = 'Pending', + Exploring = 'Exploring', Deleting = 'Deleting', AwaitingUpstream = 'AwaitingUpstream', Archived = 'Archived', } +export enum FeatureMode { + Regular = 'Regular', + Fast = 'Fast', + Exploration = 'Exploration', +} /** * Configuration for human-in-the-loop approval gates @@ -1084,9 +1094,9 @@ export type Feature = SoftDeletableEntity & { */ repositoryId?: UUID; /** - * When true, SDLC phases were skipped and the feature was implemented directly from the prompt + * Execution mode determining the workflow: Regular (full SDLC), Fast (direct implementation), or Exploration (iterative prototyping) */ - fast: boolean; + mode: FeatureMode; /** * Push branch to remote after implementation (default: false) */ @@ -1127,6 +1137,14 @@ export type Feature = SoftDeletableEntity & { * Absolute path to the git worktree for this feature */ worktreePath?: string; + /** + * Current feedback iteration count in exploration mode (0 when not exploring) + */ + iterationCount: number; + /** + * Maximum allowed iterations for exploration mode (only set when mode is Exploration) + */ + maxIterations?: number; /** * Pull request data (null until PR created) */ diff --git a/packages/core/src/domain/lifecycle-gates.ts b/packages/core/src/domain/lifecycle-gates.ts index e4a2f1348..a7c3e7582 100644 --- a/packages/core/src/domain/lifecycle-gates.ts +++ b/packages/core/src/domain/lifecycle-gates.ts @@ -1,9 +1,10 @@ /** - * Lifecycle gate constants for feature dependency blocking logic. + * Lifecycle gate constants for feature dependency blocking and + * exploration mode transition validation. * - * Centralises the POST_IMPLEMENTATION membership check used by both - * CreateFeatureUseCase (gate evaluation at creation time) and - * CheckAndUnblockFeaturesUseCase (gate evaluation at unblock time). + * Centralises membership checks used by: + * - CreateFeatureUseCase / CheckAndUnblockFeaturesUseCase (dependency gates) + * - PromoteExplorationUseCase (exploration mode transitions) */ import { SdlcLifecycle } from './generated/output'; @@ -14,11 +15,30 @@ import { SdlcLifecycle } from './generated/output'; * A parent whose lifecycle is a member of this set satisfies Gate 1: * directly-blocked children may transition from Blocked to Started. * - * Note: Pending is intentionally excluded — pending features are - * user-deferred and cannot unblock child features. + * Note: Pending and Exploring are intentionally excluded — pending features + * are user-deferred and exploring features are in prototyping mode; neither + * can unblock child features. */ export const POST_IMPLEMENTATION = new Set([ SdlcLifecycle.Implementation, SdlcLifecycle.Review, SdlcLifecycle.Maintain, ]); + +/** + * Valid lifecycle transitions FROM the Exploring state. + * + * An exploration feature may transition to: + * - Implementation: promote to Fast mode (skip SDLC, keep prototype code) + * - Requirements: promote to Regular mode (full SDLC from requirements phase) + * - Deleting: discard the exploration and clean up worktree/branch + * + * The self-loop (Exploring -> Exploring) for feedback iterations is implicit — + * the lifecycle stays Exploring during iterations, so no transition occurs. + * Exploring has no approval gates since exploration bypasses SDLC. + */ +export const EXPLORING_TRANSITIONS = new Set([ + SdlcLifecycle.Implementation, + SdlcLifecycle.Requirements, + SdlcLifecycle.Deleting, +]); diff --git a/packages/core/src/infrastructure/di/container.ts b/packages/core/src/infrastructure/di/container.ts index ae978bc50..d7612733a 100644 --- a/packages/core/src/infrastructure/di/container.ts +++ b/packages/core/src/infrastructure/di/container.ts @@ -106,6 +106,7 @@ import { ResumeFeatureUseCase } from '../../application/use-cases/features/resum import { StartFeatureUseCase } from '../../application/use-cases/features/start-feature.use-case.js'; import { UpdateFeaturePinnedConfigUseCase } from '../../application/use-cases/features/update-feature-pinned-config.use-case.js'; import { AdoptBranchUseCase } from '../../application/use-cases/features/adopt-branch.use-case.js'; +import { PromoteExplorationUseCase } from '../../application/use-cases/features/promote/promote-exploration.use-case.js'; import { GetFeatureArtifactUseCase } from '../../application/use-cases/features/get-feature-artifact.use-case.js'; import { GetResearchArtifactUseCase } from '../../application/use-cases/features/get-research-artifact.use-case.js'; import { GetPlanArtifactUseCase } from '../../application/use-cases/features/get-plan-artifact.use-case.js'; @@ -388,6 +389,7 @@ export async function initializeContainer(): Promise { container.registerSingleton(StartFeatureUseCase); container.registerSingleton(UpdateFeaturePinnedConfigUseCase); container.registerSingleton(AdoptBranchUseCase); + container.registerSingleton(PromoteExplorationUseCase); container.registerSingleton(GetFeatureArtifactUseCase); container.registerSingleton(GetResearchArtifactUseCase); container.registerSingleton(GetPlanArtifactUseCase); @@ -466,6 +468,9 @@ export async function initializeContainer(): Promise { container.register('StopAgentRunUseCase', { useFactory: (c) => c.resolve(StopAgentRunUseCase), }); + container.register('PromoteExplorationUseCase', { + useFactory: (c) => c.resolve(PromoteExplorationUseCase), + }); container.register('ApproveAgentRunUseCase', { useFactory: (c) => c.resolve(ApproveAgentRunUseCase), }); diff --git a/packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.ts b/packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.ts index 6d5605ce8..f3da14e82 100644 --- a/packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.ts +++ b/packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.ts @@ -16,6 +16,7 @@ import type { Feature } from '../../../../domain/generated/output.js'; import type { SdlcLifecycle, PrStatus, CiStatus } from '../../../../domain/generated/output.js'; +import type { FeatureMode } from '../../../../domain/generated/output.js'; /** * Database row type matching the features table schema. @@ -35,8 +36,11 @@ export interface FeatureRow { related_artifacts: string; agent_run_id: string | null; spec_path: string | null; - // Fast mode flag - fast: number; + // Feature mode + mode: string; + // Iteration tracking + iteration_count: number; + max_iterations: number | null; // Workflow configuration (flat columns) push: number; open_pr: number; @@ -101,8 +105,11 @@ export function toDatabase(feature: Feature): FeatureRow { related_artifacts: JSON.stringify(feature.relatedArtifacts), agent_run_id: feature.agentRunId ?? null, spec_path: feature.specPath ?? null, - // Fast mode flag - fast: feature.fast ? 1 : 0, + // Feature mode + mode: feature.mode, + // Iteration tracking + iteration_count: feature.iterationCount, + max_iterations: feature.maxIterations ?? null, // Flatten workflow flags to individual columns push: feature.push ? 1 : 0, open_pr: feature.openPr ? 1 : 0, @@ -173,8 +180,11 @@ export function fromDatabase(row: FeatureRow): Feature { ...(row.plan != null && { plan: JSON.parse(row.plan) }), ...(row.agent_run_id != null && { agentRunId: row.agent_run_id }), ...(row.spec_path != null && { specPath: row.spec_path }), - // Fast mode flag - fast: row.fast === 1, + // Feature mode + mode: row.mode as FeatureMode, + // Iteration tracking + iterationCount: row.iteration_count ?? 0, + ...(row.max_iterations != null && { maxIterations: row.max_iterations }), // Assemble workflow flags from flat columns push: row.push === 1, openPr: row.open_pr === 1, diff --git a/packages/core/src/infrastructure/persistence/sqlite/mappers/settings.mapper.ts b/packages/core/src/infrastructure/persistence/sqlite/mappers/settings.mapper.ts index ea08eeeb9..0f995f9c4 100644 --- a/packages/core/src/infrastructure/persistence/sqlite/mappers/settings.mapper.ts +++ b/packages/core/src/infrastructure/persistence/sqlite/mappers/settings.mapper.ts @@ -113,7 +113,7 @@ export interface SettingsRow { workflow_enable_evidence: number; workflow_commit_evidence: number; hide_ci_status: number; - default_fast_mode: number; + default_mode: string; // FeatureFlags (featureFlags.*) feature_flag_skills: number; @@ -135,6 +135,9 @@ export interface SettingsRow { // FAB layout config (added in migration 050) fab_position_swapped: number; + // Exploration max iterations (added in migration 053) + exploration_max_iterations: number | null; + // Skill injection config (added in migration 051) skill_injection_enabled: number; skill_injection_skills: string | null; @@ -224,7 +227,7 @@ export function toDatabase(settings: Settings): SettingsRow { workflow_enable_evidence: settings.workflow.enableEvidence ? 1 : 0, workflow_commit_evidence: settings.workflow.commitEvidence ? 1 : 0, hide_ci_status: settings.workflow.hideCiStatus !== false ? 1 : 0, - default_fast_mode: settings.workflow.defaultFastMode !== false ? 1 : 0, + default_mode: settings.workflow.defaultMode ?? 'Fast', // Onboarding (boolean → INTEGER) onboarding_complete: settings.onboardingComplete ? 1 : 0, @@ -260,6 +263,9 @@ export function toDatabase(settings: Settings): SettingsRow { // FAB layout config (default: not swapped) fab_position_swapped: (settings.fabLayout?.swapPosition ?? false) ? 1 : 0, + // Exploration max iterations (default: 10) + exploration_max_iterations: settings.workflow.explorationMaxIterations ?? null, + // Skill injection config (default: disabled, no skills) skill_injection_enabled: settings.workflow.skillInjection?.enabled ? 1 : 0, skill_injection_skills: settings.workflow.skillInjection?.skills?.length @@ -417,7 +423,10 @@ export function fromDatabase(row: SettingsRow): Settings { enableEvidence: row.workflow_enable_evidence === 1, commitEvidence: row.workflow_commit_evidence === 1, hideCiStatus: row.hide_ci_status === 1, - defaultFastMode: (row.default_fast_mode ?? 1) !== 0, + defaultMode: row.default_mode ?? 'Fast', + ...(row.exploration_max_iterations !== null && { + explorationMaxIterations: row.exploration_max_iterations, + }), autoArchiveDelayMinutes: row.auto_archive_delay_minutes ?? 10, }, diff --git a/packages/core/src/infrastructure/persistence/sqlite/migrations/051-replace-fast-with-mode.ts b/packages/core/src/infrastructure/persistence/sqlite/migrations/051-replace-fast-with-mode.ts new file mode 100644 index 000000000..0c376f478 --- /dev/null +++ b/packages/core/src/infrastructure/persistence/sqlite/migrations/051-replace-fast-with-mode.ts @@ -0,0 +1,56 @@ +/** + * Migration 051: Replace boolean fast column with TEXT mode column on features table. + * + * Migrates the features.fast INTEGER column (0/1 boolean) to a TEXT mode column + * that stores FeatureMode enum values: 'Regular' (was 0), 'Fast' (was 1), 'Exploration'. + * Also adds iteration_count and max_iterations columns for exploration feedback loops. + * + * Transformation logic: + * - fast = 0 → mode = 'Regular' + * - fast = 1 → mode = 'Fast' + * + * Default for new rows: mode = 'Regular', iteration_count = 0, max_iterations = NULL. + * Guards against duplicate column/dropped column using PRAGMA table_info. + */ + +import type { MigrationParams } from 'umzug'; +import type Database from 'better-sqlite3'; + +export async function up({ context: db }: MigrationParams): Promise { + const columns = db.pragma('table_info(features)') as { name: string }[]; + const names = new Set(columns.map((c) => c.name)); + + // Step 1: Add new TEXT mode column with default 'Regular' + if (!names.has('mode')) { + db.exec("ALTER TABLE features ADD COLUMN mode TEXT NOT NULL DEFAULT 'Regular'"); + } + + // Step 2: Migrate data from fast → mode (only if fast column still exists) + if (names.has('fast')) { + db.exec(` + UPDATE features + SET mode = CASE + WHEN fast = 1 THEN 'Fast' + ELSE 'Regular' + END + `); + } + + // Step 3: Drop old fast column + if (names.has('fast')) { + db.exec('ALTER TABLE features DROP COLUMN fast'); + } + + // Step 4: Add iteration tracking columns for exploration mode + if (!names.has('iteration_count')) { + db.exec('ALTER TABLE features ADD COLUMN iteration_count INTEGER NOT NULL DEFAULT 0'); + } + + if (!names.has('max_iterations')) { + db.exec('ALTER TABLE features ADD COLUMN max_iterations INTEGER'); + } +} + +export async function down({ context: db }: MigrationParams): Promise { + void db; +} diff --git a/packages/core/src/infrastructure/persistence/sqlite/migrations/052-replace-default-fast-mode-with-default-mode.ts b/packages/core/src/infrastructure/persistence/sqlite/migrations/052-replace-default-fast-mode-with-default-mode.ts new file mode 100644 index 000000000..daec6afc0 --- /dev/null +++ b/packages/core/src/infrastructure/persistence/sqlite/migrations/052-replace-default-fast-mode-with-default-mode.ts @@ -0,0 +1,45 @@ +/** + * Migration 052: Replace boolean default_fast_mode column with TEXT default_mode column + * on the settings table. + * + * Migrates the settings.default_fast_mode INTEGER column (0/1 boolean) to a TEXT + * default_mode column that stores FeatureMode enum values: + * - default_fast_mode = 1 → default_mode = 'Fast' + * - default_fast_mode = 0 → default_mode = 'Regular' + * + * Default for new rows: default_mode = 'Fast' (preserves backward compatibility). + * Guards against duplicate column using PRAGMA table_info. + */ + +import type { MigrationParams } from 'umzug'; +import type Database from 'better-sqlite3'; + +export async function up({ context: db }: MigrationParams): Promise { + const columns = db.pragma('table_info(settings)') as { name: string }[]; + const names = new Set(columns.map((c) => c.name)); + + // Step 1: Add new TEXT default_mode column with default 'Fast' + if (!names.has('default_mode')) { + db.exec("ALTER TABLE settings ADD COLUMN default_mode TEXT NOT NULL DEFAULT 'Fast'"); + } + + // Step 2: Migrate data from default_fast_mode → default_mode + if (names.has('default_fast_mode')) { + db.exec(` + UPDATE settings + SET default_mode = CASE + WHEN default_fast_mode = 1 THEN 'Fast' + ELSE 'Regular' + END + `); + } + + // Step 3: Drop old default_fast_mode column + if (names.has('default_fast_mode')) { + db.exec('ALTER TABLE settings DROP COLUMN default_fast_mode'); + } +} + +export async function down({ context: db }: MigrationParams): Promise { + void db; +} diff --git a/packages/core/src/infrastructure/persistence/sqlite/migrations/053-add-exploration-max-iterations.ts b/packages/core/src/infrastructure/persistence/sqlite/migrations/053-add-exploration-max-iterations.ts new file mode 100644 index 000000000..4900f2e13 --- /dev/null +++ b/packages/core/src/infrastructure/persistence/sqlite/migrations/053-add-exploration-max-iterations.ts @@ -0,0 +1,23 @@ +/** + * Migration 053: Add exploration_max_iterations column to the settings table. + * + * Adds a nullable INTEGER column for the maximum number of feedback iterations + * in exploration mode. When NULL, the default of 10 is applied at the application layer. + * Guards against duplicate column using PRAGMA table_info. + */ + +import type { MigrationParams } from 'umzug'; +import type Database from 'better-sqlite3'; + +export async function up({ context: db }: MigrationParams): Promise { + const columns = db.pragma('table_info(settings)') as { name: string }[]; + const names = new Set(columns.map((c) => c.name)); + + if (!names.has('exploration_max_iterations')) { + db.exec('ALTER TABLE settings ADD COLUMN exploration_max_iterations INTEGER'); + } +} + +export async function down({ context: db }: MigrationParams): Promise { + void db; +} diff --git a/packages/core/src/infrastructure/repositories/sqlite-feature.repository.ts b/packages/core/src/infrastructure/repositories/sqlite-feature.repository.ts index 3bf208054..101e7fd7e 100644 --- a/packages/core/src/infrastructure/repositories/sqlite-feature.repository.ts +++ b/packages/core/src/infrastructure/repositories/sqlite-feature.repository.ts @@ -35,7 +35,7 @@ export class SQLiteFeatureRepository implements IFeatureRepository { id, name, slug, description, user_query, repository_path, branch, lifecycle, messages, plan, related_artifacts, agent_run_id, spec_path, - fast, + mode, iteration_count, max_iterations, push, open_pr, fork_and_pr, commit_specs, ci_watch_enabled, enable_evidence, commit_evidence, auto_merge, allow_prd, allow_plan, allow_merge, @@ -50,7 +50,7 @@ export class SQLiteFeatureRepository implements IFeatureRepository { @id, @name, @slug, @description, @user_query, @repository_path, @branch, @lifecycle, @messages, @plan, @related_artifacts, @agent_run_id, @spec_path, - @fast, + @mode, @iteration_count, @max_iterations, @push, @open_pr, @fork_and_pr, @commit_specs, @ci_watch_enabled, @enable_evidence, @commit_evidence, @auto_merge, @allow_prd, @allow_plan, @allow_merge, @@ -173,7 +173,9 @@ export class SQLiteFeatureRepository implements IFeatureRepository { related_artifacts = @related_artifacts, agent_run_id = @agent_run_id, spec_path = @spec_path, - fast = @fast, + mode = @mode, + iteration_count = @iteration_count, + max_iterations = @max_iterations, push = @push, open_pr = @open_pr, fork_and_pr = @fork_and_pr, diff --git a/packages/core/src/infrastructure/repositories/sqlite-settings.repository.ts b/packages/core/src/infrastructure/repositories/sqlite-settings.repository.ts index c9c86cd0d..da162ef7c 100644 --- a/packages/core/src/infrastructure/repositories/sqlite-settings.repository.ts +++ b/packages/core/src/infrastructure/repositories/sqlite-settings.repository.ts @@ -69,7 +69,7 @@ export class SQLiteSettingsRepository implements ISettingsRepository { feature_flag_react_file_manager, feature_flag_inventory, workflow_enable_evidence, workflow_commit_evidence, - hide_ci_status, default_fast_mode, + hide_ci_status, default_mode, interactive_agent_enabled, interactive_agent_auto_timeout_minutes, interactive_agent_max_concurrent_sessions, auto_archive_delay_minutes, @@ -101,7 +101,7 @@ export class SQLiteSettingsRepository implements ISettingsRepository { @feature_flag_react_file_manager, @feature_flag_inventory, @workflow_enable_evidence, @workflow_commit_evidence, - @hide_ci_status, @default_fast_mode, + @hide_ci_status, @default_mode, @interactive_agent_enabled, @interactive_agent_auto_timeout_minutes, @interactive_agent_max_concurrent_sessions, @auto_archive_delay_minutes, @@ -214,7 +214,7 @@ export class SQLiteSettingsRepository implements ISettingsRepository { workflow_enable_evidence = @workflow_enable_evidence, workflow_commit_evidence = @workflow_commit_evidence, hide_ci_status = @hide_ci_status, - default_fast_mode = @default_fast_mode, + default_mode = @default_mode, interactive_agent_enabled = @interactive_agent_enabled, interactive_agent_auto_timeout_minutes = @interactive_agent_auto_timeout_minutes, interactive_agent_max_concurrent_sessions = @interactive_agent_max_concurrent_sessions, diff --git a/packages/core/src/infrastructure/services/agents/feature-agent/exploration-agent-graph.ts b/packages/core/src/infrastructure/services/agents/feature-agent/exploration-agent-graph.ts new file mode 100644 index 000000000..1474e6fe5 --- /dev/null +++ b/packages/core/src/infrastructure/services/agents/feature-agent/exploration-agent-graph.ts @@ -0,0 +1,94 @@ +/** + * Exploration Agent Graph Factory + * + * Creates a LangGraph StateGraph for exploration/prototyping mode with + * the topology: + * START → prototype-generate → (interrupt for feedback) + * → apply-feedback → prototype-generate (loop) + * → END (on promote or discard) + * + * The graph uses the same FeatureAgentAnnotation state shape as the + * full and fast graphs, enabling checkpointing, resume, and identical + * worker lifecycle handling. + * + * Unlike the full SDLC graph which uses executeNode() with approval gates, + * the exploration graph uses custom node functions with shared utilities + * (retryExecute, createNodeLogger). The feedback loop is driven by + * LangGraph interrupt/resume — the same mechanism used for approval gates. + */ + +import { StateGraph, START, type BaseCheckpointSaver } from '@langchain/langgraph'; +import type { IAgentExecutor } from '@/application/ports/output/agents/agent-executor.interface.js'; +import { FeatureAgentAnnotation, type FeatureAgentState } from './state.js'; +import { createPrototypeGenerateNode } from './nodes/prototype-generate.node.js'; +import { createApplyFeedbackNode } from './nodes/apply-feedback.node.js'; + +// Re-export for consumers +export { FeatureAgentAnnotation, type FeatureAgentState } from './state.js'; + +/** + * Dependencies needed to build the exploration agent graph. + * Intentionally minimal — exploration mode does not use merge nodes. + */ +export interface ExplorationAgentGraphDeps { + executor: IAgentExecutor; +} + +/** + * Routing function that determines the next node after prototype-generate + * resumes from interrupt. + * + * The resume payload (set via Command({update})) controls the flow: + * - _approvalAction === 'rejected' → apply-feedback (iterate with feedback) + * - _approvalAction === 'approved' or any other → END (promote or discard) + * + * The 'rejected' action maps to the iterate flow because the existing + * worker resume logic already sets _approvalAction='rejected' and + * _rejectionFeedback= for rejection payloads. For exploration, + * "rejected" means "iterate with feedback" rather than "rejected for merge". + */ +export function routeAfterPrototypeGenerate(state: FeatureAgentState): string { + // If the user provided feedback (rejection = iterate in exploration context) + if (state._approvalAction === 'rejected') { + return 'apply-feedback'; + } + // Otherwise: approved = promote/discard = exit the loop + return '__end__'; +} + +/** + * Factory function that creates and compiles the exploration-mode agent graph. + * + * The graph defines a feedback loop: + * START → prototype-generate → (interrupt) → route: + * - iterate: → apply-feedback → prototype-generate (loop) + * - promote/discard: → END + * + * @param depsOrExecutor - Graph dependencies or a legacy executor + * @param checkpointer - Optional checkpoint saver for state persistence + * @returns A compiled LangGraph ready to be invoked + */ +export function createExplorationAgentGraph( + depsOrExecutor: ExplorationAgentGraphDeps | IAgentExecutor, + checkpointer?: BaseCheckpointSaver +) { + // Support legacy signature: createExplorationAgentGraph(executor, checkpointer) + const deps: ExplorationAgentGraphDeps = + 'execute' in depsOrExecutor ? { executor: depsOrExecutor } : depsOrExecutor; + const { executor } = deps; + + const graph = new StateGraph(FeatureAgentAnnotation) + .addNode('prototype-generate', createPrototypeGenerateNode(executor)) + .addNode('apply-feedback', createApplyFeedbackNode()); + + // START → prototype-generate + graph.addEdge(START, 'prototype-generate'); + + // prototype-generate → conditional routing (after resume from interrupt) + graph.addConditionalEdges('prototype-generate', routeAfterPrototypeGenerate); + + // apply-feedback → prototype-generate (loop back) + graph.addEdge('apply-feedback', 'prototype-generate'); + + return graph.compile({ checkpointer }); +} diff --git a/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.ts b/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.ts index bf35d8171..bf3a20367 100644 --- a/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.ts +++ b/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.ts @@ -15,7 +15,12 @@ import { homedir } from 'node:os'; import { mkdirSync } from 'node:fs'; import type { IFeatureAgentProcessService } from '@/application/ports/output/agents/feature-agent-process.interface.js'; import type { IAgentRunRepository } from '@/application/ports/output/agents/agent-run-repository.interface.js'; -import { AgentRunStatus, type ApprovalGates, type AgentType } from '@/domain/generated/output.js'; +import { + AgentRunStatus, + FeatureMode, + type ApprovalGates, + type AgentType, +} from '@/domain/generated/output.js'; import { IS_WINDOWS } from '../../../platform.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -51,7 +56,7 @@ export class FeatureAgentProcessService implements IFeatureAgentProcessService { commitEvidence?: boolean; resumePayload?: string; agentType?: AgentType; - fast?: boolean; + mode?: FeatureMode; model?: string; resumeReason?: string; } @@ -110,8 +115,8 @@ export class FeatureAgentProcessService implements IFeatureAgentProcessService { if (options?.agentType) { args.push('--agent-type', options.agentType); } - if (options?.fast) { - args.push('--fast'); + if (options?.mode && options.mode !== FeatureMode.Regular) { + args.push('--mode', options.mode); } if (options?.model) { args.push('--model', options.model); diff --git a/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.ts b/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.ts index 4ea4afa82..cdedf8e68 100644 --- a/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.ts +++ b/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.ts @@ -18,6 +18,8 @@ import { createFeatureAgentGraph } from './feature-agent-graph.js'; import type { FeatureAgentGraphDeps } from './feature-agent-graph.js'; import { createFastFeatureAgentGraph } from './fast-feature-agent-graph.js'; import type { FastFeatureAgentGraphDeps } from './fast-feature-agent-graph.js'; +import { createExplorationAgentGraph } from './exploration-agent-graph.js'; +import type { ExplorationAgentGraphDeps } from './exploration-agent-graph.js'; import { createCheckpointer } from '../common/checkpointer.js'; import type { IAgentRunRepository } from '@/application/ports/output/agents/agent-run-repository.interface.js'; import type { IAgentExecutorProvider } from '@/application/ports/output/agents/agent-executor-provider.interface.js'; @@ -25,7 +27,12 @@ import type { IAgentExecutorFactory } from '@/application/ports/output/agents/ag import type { IFeatureRepository } from '@/application/ports/output/repositories/feature-repository.interface.js'; import type { IGitPrService } from '@/application/ports/output/services/git-pr-service.interface.js'; import type { IGitForkService } from '@/application/ports/output/services/git-fork-service.interface.js'; -import { AgentRunStatus, SdlcLifecycle, type AgentType } from '@/domain/generated/output.js'; +import { + AgentRunStatus, + SdlcLifecycle, + FeatureMode, + type AgentType, +} from '@/domain/generated/output.js'; import { initializeSettings } from '@/infrastructure/services/settings.service.js'; import { InitializeSettingsUseCase } from '@/application/use-cases/settings/initialize-settings.use-case.js'; import { setHeartbeatContext } from './heartbeat.js'; @@ -57,7 +64,7 @@ export interface WorkerArgs { commitEvidence?: boolean; resumePayload?: string; agentType?: AgentType; - fast?: boolean; + mode?: FeatureMode; model?: string; resumeReason?: string; } @@ -99,7 +106,11 @@ export function parseWorkerArgs(args: string[]): WorkerArgs { const ciWatchEnabled = !args.includes('--no-ci-watch'); const enableEvidence = args.includes('--enable-evidence'); const commitEvidence = args.includes('--commit-evidence'); - const fast = args.includes('--fast'); + const modeIdx = args.indexOf('--mode'); + const mode: FeatureMode = + modeIdx !== -1 && modeIdx + 1 < args.length + ? (args[modeIdx + 1] as FeatureMode) + : FeatureMode.Regular; const threadIdx = args.indexOf('--thread-id'); const threadId = threadIdx !== -1 && threadIdx + 1 < args.length ? args[threadIdx + 1] : undefined; @@ -144,7 +155,7 @@ export function parseWorkerArgs(args: string[]): WorkerArgs { commitEvidence, resumePayload, agentType, - fast, + mode, model, resumeReason, }; @@ -211,7 +222,7 @@ export async function runWorker(args: WorkerArgs): Promise { ...(args.commitSpecs === false ? ['--no-commit-specs'] : []), ...(args.resumePayload ? ['--resume-payload', args.resumePayload] : []), ...(args.agentType ? ['--agent-type', args.agentType] : []), - ...(args.fast ? ['--fast'] : []), + ...(args.mode && args.mode !== FeatureMode.Regular ? ['--mode', args.mode] : []), ...(args.model ? ['--model', args.model] : []), ]; log(`Starting worker — full command:`); @@ -279,15 +290,23 @@ export async function runWorker(args: WorkerArgs): Promise { const checkpointPath = join(homedir(), '.shep', 'checkpoints', `${checkpointId}.db`); log(`Creating checkpointer at ${checkpointPath} (thread: ${checkpointId})`); const checkpointer = createCheckpointer(checkpointPath); - // Both graph factories return compiled graphs with identical FeatureAgentAnnotation + // All graph factories return compiled graphs with identical FeatureAgentAnnotation // state shape and invoke() interface. Cast through unknown because the compiled // graphs have different node name types but share the same runtime contract. - const graph = args.fast - ? (createFastFeatureAgentGraph( - graphDeps as FastFeatureAgentGraphDeps, - checkpointer - ) as unknown as ReturnType) - : createFeatureAgentGraph(graphDeps, checkpointer); + let graph: ReturnType; + if (args.mode === FeatureMode.Exploration) { + graph = createExplorationAgentGraph( + { executor } as ExplorationAgentGraphDeps, + checkpointer + ) as unknown as ReturnType; + } else if (args.mode === FeatureMode.Fast) { + graph = createFastFeatureAgentGraph( + graphDeps as FastFeatureAgentGraphDeps, + checkpointer + ) as unknown as ReturnType; + } else { + graph = createFeatureAgentGraph(graphDeps, checkpointer); + } // Mark the run as running with our PID const now = new Date(); diff --git a/packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.ts b/packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.ts index b88a81637..494d99c68 100644 --- a/packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.ts +++ b/packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.ts @@ -37,6 +37,7 @@ const NODE_TO_LIFECYCLE: Record = { plan: SdlcLifecycle.Planning, implement: SdlcLifecycle.Implementation, 'fast-implement': SdlcLifecycle.Implementation, + 'prototype-generate': SdlcLifecycle.Exploring, merge: SdlcLifecycle.Review, }; diff --git a/packages/core/src/infrastructure/services/agents/feature-agent/nodes/apply-feedback.node.ts b/packages/core/src/infrastructure/services/agents/feature-agent/nodes/apply-feedback.node.ts new file mode 100644 index 000000000..0f374cb7a --- /dev/null +++ b/packages/core/src/infrastructure/services/agents/feature-agent/nodes/apply-feedback.node.ts @@ -0,0 +1,59 @@ +/** + * Apply-Feedback Node + * + * Exploration mode state-transformation node that receives user feedback + * from the resume payload, appends it to feedbackHistory, and prepares + * context for the next prototype-generate iteration. + * + * This node does NOT call the agent executor — it is a pure state + * transformation that processes the feedback and returns updated state. + */ + +import type { FeatureAgentState } from '../state.js'; +import { createNodeLogger } from './node-helpers.js'; +import { reportNodeStart } from '../heartbeat.js'; +import { buildApplyFeedbackContext } from './prompts/apply-feedback.prompt.js'; + +/** + * Factory that creates the apply-feedback node function. + * + * @returns A LangGraph node function + */ +export function createApplyFeedbackNode() { + const log = createNodeLogger('apply-feedback'); + + return async (state: FeatureAgentState): Promise> => { + log.activate(); + log.info('Processing user feedback'); + reportNodeStart('apply-feedback'); + + // Extract feedback from the resume state. + // The worker sets _rejectionFeedback from the resume payload, + // or the feedback may come through a dedicated exploration channel. + const feedback = state._rejectionFeedback ?? ''; + const iterationCount = state.iterationCount ?? 0; + + if (!feedback || feedback.trim().length === 0) { + log.info('No feedback text provided — proceeding with empty feedback'); + } else { + log.info(`Feedback received (${feedback.length} chars) for iteration ${iterationCount}`); + } + + // Build context summary for logging + const contextSummary = buildApplyFeedbackContext(state, feedback); + log.info(`Context prepared for next iteration`); + + return { + currentNode: 'apply-feedback', + explorationStatus: 'applying-feedback', + feedbackHistory: [feedback], + // Clear the rejection feedback after consuming it + _rejectionFeedback: null, + _approvalAction: null, + messages: [ + `[apply-feedback] Feedback applied for iteration ${iterationCount + 1}: "${feedback.slice(0, 100)}${feedback.length > 100 ? '...' : ''}"`, + `[apply-feedback] Context: ${contextSummary.slice(0, 200)}`, + ], + }; + }; +} diff --git a/packages/core/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.ts b/packages/core/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.ts index 7265c4a65..3222c08bb 100644 --- a/packages/core/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.ts +++ b/packages/core/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.ts @@ -76,6 +76,7 @@ const STAGE_TIMEOUT_KEY: Record = { plan: 'planMs', implement: 'implementMs', 'fast-implement': 'fastImplementMs', + 'prototype-generate': 'implementMs', evidence: 'implementMs', merge: 'mergeMs', }; diff --git a/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/apply-feedback.prompt.ts b/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/apply-feedback.prompt.ts new file mode 100644 index 000000000..e85beceb4 --- /dev/null +++ b/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/apply-feedback.prompt.ts @@ -0,0 +1,48 @@ +/** + * Apply-Feedback Prompt Builder + * + * Builds a context summary for the apply-feedback node. This is used + * internally by the graph to prepare context for the next + * prototype-generate iteration — it does NOT call the executor. + * + * The prompt text produced here serves as a structured summary of + * the current feedback and iteration context that the apply-feedback + * node stores in state for the next prototype-generate node to consume. + */ + +import type { FeatureAgentState } from '../../state.js'; + +/** + * Build a context summary for the apply-feedback state transformation. + * + * Includes: + * 1. Current feedback text + * 2. Iteration count + * 3. Summary of prior feedback history + * + * This is stored in graph state, not sent to an executor. + */ +export function buildApplyFeedbackContext( + state: FeatureAgentState, + currentFeedback: string +): string { + const iterationCount = state.iterationCount ?? 0; + const feedbackHistory = state.feedbackHistory ?? []; + + const sections: string[] = []; + + sections.push(`## Feedback Applied — Iteration ${iterationCount + 1}`); + sections.push(`**Current feedback:** ${currentFeedback}`); + + if (feedbackHistory.length > 0) { + sections.push(`**Prior feedback rounds:** ${feedbackHistory.length}`); + const recentPrior = feedbackHistory.slice(-3); + const startIdx = feedbackHistory.length - recentPrior.length; + recentPrior.forEach((fb, i) => { + const summary = fb.length > 80 ? `${fb.slice(0, 80)}...` : fb; + sections.push(`- Iteration ${startIdx + i + 1}: ${summary}`); + }); + } + + return sections.join('\n'); +} diff --git a/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/prototype-generate.prompt.ts b/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/prototype-generate.prompt.ts new file mode 100644 index 000000000..cf1a65b34 --- /dev/null +++ b/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/prototype-generate.prompt.ts @@ -0,0 +1,257 @@ +/** + * Prototype-Generate Prompt Builder + * + * Builds the prompt for exploration mode prototype generation. + * Emphasizes quick prototyping, minimal scope, and throwaway quality. + * Includes iteration context (feedback history) for subsequent rounds. + * + * The prompt instructs the executor to produce a working prototype that + * demonstrates the idea — prioritizing concept demonstration over + * production quality. + */ + +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { join } from 'node:path'; +import yaml from 'js-yaml'; +import { readSpecFile } from '../node-helpers.js'; +import type { FeatureAgentState } from '../../state.js'; + +/** + * Read a file from the worktree, returning empty string if not found. + */ +function readWorktreeFile(worktreePath: string, filename: string): string { + try { + return readFileSync(join(worktreePath, filename), 'utf-8'); + } catch { + return ''; + } +} + +/** + * Build a shallow directory listing (depth 1-2) of the worktree. + * Returns a tree-style string. Skips common noise directories. + */ +function buildDirectoryListing(worktreePath: string): string { + const SKIP_DIRS = new Set([ + 'node_modules', + '.git', + '.next', + 'dist', + 'build', + 'coverage', + '.turbo', + '.cache', + '.shep', + ]); + + const lines: string[] = []; + + try { + const topLevel = readdirSync(worktreePath); + for (const entry of topLevel) { + if (SKIP_DIRS.has(entry)) continue; + + const fullPath = join(worktreePath, entry); + let isDir = false; + try { + isDir = statSync(fullPath).isDirectory(); + } catch { + continue; + } + + if (isDir) { + lines.push(`${entry}/`); + try { + const children = readdirSync(fullPath); + for (const child of children) { + if (SKIP_DIRS.has(child)) continue; + const childPath = join(fullPath, child); + let childIsDir = false; + try { + childIsDir = statSync(childPath).isDirectory(); + } catch { + continue; + } + lines.push(` ${child}${childIsDir ? '/' : ''}`); + } + } catch { + // Cannot read subdirectory — skip + } + } else { + lines.push(entry); + } + } + } catch { + return '(unable to list directory)'; + } + + return lines.join('\n'); +} + +/** + * Extract the user query from spec.yaml's userQuery field. + * Falls back to the raw YAML content if parsing fails. + */ +function extractUserQuery(specDir: string): string { + const specContent = readSpecFile(specDir, 'spec.yaml'); + if (!specContent) { + // For exploration mode, also check feature.yaml for the prompt + const featureContent = readSpecFile(specDir, 'feature.yaml'); + if (!featureContent) return ''; + try { + const data = yaml.load(featureContent) as Record; + const userQuery = data?.userQuery ?? (data?.feature as Record)?.description; + if (typeof userQuery === 'string' && userQuery.trim()) { + return userQuery.trim(); + } + } catch { + // Fall through + } + return featureContent; + } + + try { + const data = yaml.load(specContent) as Record; + const userQuery = data?.userQuery; + if (typeof userQuery === 'string' && userQuery.trim()) { + return userQuery.trim(); + } + } catch { + // Fall through to raw content + } + + return specContent; +} + +/** Maximum number of recent feedback entries to include in full detail. */ +const MAX_FULL_FEEDBACK_ENTRIES = 3; + +/** + * Build a feedback history section for the prompt. + * Recent feedback (last 3) gets full detail; older entries are summarized. + */ +export function buildFeedbackHistorySection(feedbackHistory: string[]): string { + if (feedbackHistory.length === 0) return ''; + + const sections: string[] = []; + + if (feedbackHistory.length > MAX_FULL_FEEDBACK_ENTRIES) { + const older = feedbackHistory.slice(0, -MAX_FULL_FEEDBACK_ENTRIES); + sections.push('### Earlier feedback (summarized)'); + older.forEach((fb, i) => { + // Truncate each older entry to a single line summary + const summary = fb.length > 100 ? `${fb.slice(0, 100)}...` : fb; + sections.push(`- Iteration ${i + 1}: ${summary}`); + }); + sections.push(''); + } + + const recent = feedbackHistory.slice(-MAX_FULL_FEEDBACK_ENTRIES); + const startIdx = feedbackHistory.length - recent.length; + sections.push('### Recent feedback (act on these)'); + recent.forEach((fb, i) => { + sections.push(`**Iteration ${startIdx + i + 1}:** ${fb}`); + }); + + return `## Feedback History\n\n${sections.join('\n')}`; +} + +/** + * Build the prototype-generate prompt. + * + * The prompt includes: + * 1. Exploration mode instructions (speed over quality) + * 2. User's idea/query + * 3. Iteration context (feedback history from prior rounds) + * 4. CLAUDE.md content (if exists) + * 5. Shallow directory listing + * + * Instructs the executor to produce a quick, working prototype. + */ +export function buildPrototypeGeneratePrompt(state: FeatureAgentState): string { + const cwd = state.worktreePath || state.repositoryPath; + const userQuery = extractUserQuery(state.specDir); + const iterationCount = state.iterationCount ?? 0; + const feedbackHistory = state.feedbackHistory ?? []; + + // Read optional context files + const claudeMd = readWorktreeFile(cwd, 'CLAUDE.md'); + const dirListing = buildDirectoryListing(cwd); + + const isFirstIteration = iterationCount === 0; + + const sections: string[] = []; + + // Main instruction + sections.push(`You are a senior software engineer in EXPLORATION MODE — generating a quick prototype. + +## Mode: Exploration (Prototype) + +**Priority: SPEED over quality. Show the concept, don't build for production.** + +- Generate a working prototype that demonstrates the idea +- Keep scope minimal — focus on the core concept +- Skip edge cases, error handling, and production polish +- Write just enough code to show how the idea would work +- This is throwaway code — it will be rewritten if the idea is promoted to a real feature`); + + // User request + if (isFirstIteration) { + sections.push(`## User's Idea + +${userQuery}`); + } else { + sections.push(`## User's Idea (Original) + +${userQuery} + +## Current Iteration: ${iterationCount + 1} + +This is iteration ${iterationCount + 1} of the prototype. Review the feedback below and update the prototype accordingly.`); + } + + // Feedback history + const feedbackSection = buildFeedbackHistorySection(feedbackHistory); + if (feedbackSection) { + sections.push(feedbackSection); + } + + // Implementation instructions + sections.push(`## Implementation Instructions + +1. ${isFirstIteration ? 'Create' : 'Update'} the prototype based on ${isFirstIteration ? 'the idea above' : 'the feedback above'} +2. Make the code functional — it should compile and demonstrate the concept +3. Commit your work with a conventional commit message (e.g. \`feat(domain): prototype workspace grouping\`) +4. Do NOT write tests — this is a prototype +5. Do NOT run linters or builds — speed is the priority +6. Do NOT push to remote + +## Working Directory + +${cwd}`); + + if (claudeMd) { + sections.push(`## Project Guidelines (CLAUDE.md) + +${claudeMd}`); + } + + if (dirListing && dirListing !== '(unable to list directory)') { + sections.push(`## Project Structure + +\`\`\` +${dirListing} +\`\`\``); + } + + sections.push(`## Constraints + +- SPEED is the priority — generate a working prototype quickly +- Keep changes minimal and focused on demonstrating the concept +- Do NOT modify any spec YAML files +- Do NOT enter plan mode — implement directly +- Do NOT ask questions — make reasonable assumptions and proceed +- You MUST create or modify actual code files — a plan or summary alone is not acceptable`); + + return sections.join('\n\n'); +} diff --git a/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prototype-generate.node.ts b/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prototype-generate.node.ts new file mode 100644 index 000000000..c2edbc12c --- /dev/null +++ b/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prototype-generate.node.ts @@ -0,0 +1,150 @@ +/** + * Prototype-Generate Node + * + * Exploration mode node that generates a quick prototype by calling the + * agent executor with an exploration-focused prompt. After generation, + * interrupts for user feedback. Uses shared utilities (retryExecute, + * createNodeLogger) but NOT executeNode() which is coupled to approval gates. + * + * Follows the same factory pattern as other nodes: takes executor + * dependency, returns async (state) => Partial. + */ + +import { interrupt, isGraphBubbleUp } from '@langchain/langgraph'; +import type { IAgentExecutor } from '@/application/ports/output/agents/agent-executor.interface.js'; +import type { FeatureAgentState } from '../state.js'; +import { createNodeLogger, buildExecutorOptions, retryExecute } from './node-helpers.js'; +import { reportNodeStart } from '../heartbeat.js'; +import { recordPhaseStart, recordPhaseEnd } from '../phase-timing-context.js'; +import { updateNodeLifecycle } from '../lifecycle-context.js'; +import { buildPrototypeGeneratePrompt } from './prompts/prototype-generate.prompt.js'; + +/** + * Factory that creates the prototype-generate node function. + * + * @param executor - The agent executor to use for prototype generation + * @returns A LangGraph node function + */ +export function createPrototypeGenerateNode(executor: IAgentExecutor) { + const log = createNodeLogger('prototype-generate'); + + return async (state: FeatureAgentState): Promise> => { + log.activate(); + const iterationCount = state.iterationCount ?? 0; + const maxIterations = state.maxIterations ?? 10; + + log.info(`Starting prototype generation (iteration ${iterationCount + 1}/${maxIterations})`); + reportNodeStart('prototype-generate'); + await updateNodeLifecycle('prototype-generate'); + + // On resume from interrupt, LangGraph re-executes the node function. + // If _approvalAction is set, the worker has resumed us with a user action. + // Return early and let the conditional edge route to apply-feedback or END. + if (state._approvalAction !== null && state._approvalAction !== undefined) { + if (state._approvalAction === 'rejected') { + log.info('Resumed with feedback — routing to apply-feedback'); + return { + currentNode: 'prototype-generate', + explorationStatus: 'applying-feedback', + messages: ['[prototype-generate] Resumed — routing feedback to apply-feedback'], + }; + } else { + log.info('Resumed with promote/discard — routing to END'); + return { + currentNode: 'prototype-generate', + explorationStatus: 'promoting', + messages: ['[prototype-generate] Resumed — routing to END (promote/discard)'], + _approvalAction: null, + }; + } + } + + // Check if max iterations reached — force user to promote or discard + if (iterationCount >= maxIterations) { + log.info(`Max iterations (${maxIterations}) reached — interrupting for final action`); + interrupt({ + node: 'prototype-generate', + message: `Maximum iterations (${maxIterations}) reached. Please promote or discard this exploration.`, + iterationCount, + maxIterations, + forceAction: true, + }); + + // If we get here after resume, the graph router will handle the action + return { + currentNode: 'prototype-generate', + explorationStatus: 'waiting-feedback', + messages: [ + `[prototype-generate] Max iterations reached (${maxIterations}) — awaiting promote/discard`, + ], + }; + } + + const startTime = Date.now(); + const prompt = buildPrototypeGeneratePrompt(state); + const timingId = await recordPhaseStart('prototype-generate', { + prompt, + modelId: state.model, + agentType: executor.agentType, + }); + + try { + const options = buildExecutorOptions(state, undefined, 'prototype-generate'); + log.info(`Executing agent at cwd=${options.cwd}`); + log.info(`Prompt length: ${prompt.length} chars`); + + const result = await retryExecute(executor, prompt, options, { logger: log }); + const durationMs = Date.now() - startTime; + const elapsed = (durationMs / 1000).toFixed(1); + log.info(`Prototype generated (${result.result.length} chars, ${elapsed}s)`); + + await recordPhaseEnd(timingId, durationMs, { + inputTokens: result.usage?.inputTokens, + outputTokens: result.usage?.outputTokens, + cacheCreationInputTokens: result.usage?.cacheCreationInputTokens, + cacheReadInputTokens: result.usage?.cacheReadInputTokens, + costUsd: result.usage?.costUsd, + numTurns: result.usage?.numTurns, + durationApiMs: result.usage?.durationApiMs, + exitCode: 'success', + }); + + const newIterationCount = iterationCount + 1; + + // Interrupt for user feedback + log.info(`Interrupting for feedback (iteration ${newIterationCount}/${maxIterations})`); + interrupt({ + node: 'prototype-generate', + message: `Prototype iteration ${newIterationCount} complete. Review and provide feedback, promote, or discard.`, + iterationCount: newIterationCount, + maxIterations, + resultSummary: result.result.slice(0, 500), + }); + + // After resume, return the updated state + return { + currentNode: 'prototype-generate', + iterationCount: newIterationCount, + explorationStatus: 'waiting-feedback', + messages: [ + `[prototype-generate] Iteration ${newIterationCount} complete (${result.result.length} chars, ${elapsed}s)`, + ], + }; + } catch (err: unknown) { + if (isGraphBubbleUp(err)) throw err; + + const message = err instanceof Error ? err.message : String(err); + const durationMs = Date.now() - startTime; + const elapsed = (durationMs / 1000).toFixed(1); + log.error(`${message} (after ${elapsed}s)`); + + await recordPhaseEnd(timingId, durationMs, { + exitCode: 'error', + errorMessage: message.slice(0, 1000), + }); + + // Throw so LangGraph does NOT checkpoint this node as "completed" + throw new Error(`[prototype-generate] ${message}`); + } + }; +} diff --git a/packages/core/src/infrastructure/services/agents/feature-agent/state.ts b/packages/core/src/infrastructure/services/agents/feature-agent/state.ts index 5233211b8..d3f1a5987 100644 --- a/packages/core/src/infrastructure/services/agents/feature-agent/state.ts +++ b/packages/core/src/infrastructure/services/agents/feature-agent/state.ts @@ -128,6 +128,25 @@ export const FeatureAgentAnnotation = Annotation.Root({ reducer: (_prev, next) => next, default: () => 'idle', }), + // --- Exploration mode state channels --- + iterationCount: Annotation({ + reducer: (_prev, next) => next, + default: () => 0, + }), + maxIterations: Annotation({ + reducer: (_prev, next) => next, + default: () => 10, + }), + feedbackHistory: Annotation({ + reducer: (prev, next) => [...prev, ...next], + default: () => [], + }), + explorationStatus: Annotation< + 'generating' | 'waiting-feedback' | 'applying-feedback' | 'promoting' | 'discarding' | undefined + >({ + reducer: (_prev, next) => next, + default: () => undefined, + }), }); export type FeatureAgentState = typeof FeatureAgentAnnotation.State; diff --git a/packages/core/src/infrastructure/services/spec/spec-initializer.service.ts b/packages/core/src/infrastructure/services/spec/spec-initializer.service.ts index 7702de295..9dcc67bce 100644 --- a/packages/core/src/infrastructure/services/spec/spec-initializer.service.ts +++ b/packages/core/src/infrastructure/services/spec/spec-initializer.service.ts @@ -271,7 +271,7 @@ export class SpecInitializerService implements ISpecInitializerService { slug: string, featureNumber: number, description: string, - mode?: 'fast' + mode?: 'fast' | 'exploration' ): Promise { // Scan existing specs/ directory for highest NNN prefix to avoid collisions // (specs may have been created outside the DB, e.g., via /shep-kit:new-feature) @@ -291,13 +291,16 @@ export class SpecInitializerService implements ISpecInitializerService { const vars = { nnn, slug, featureNumber: resolvedNumber, date, timestamp, description }; // In fast mode, write feature.yaml + spec.yaml (spec.yaml stores the user query - // which the fast-implement node reads; no research/plan/tasks needed) + // which the fast-implement node reads; no research/plan/tasks needed). + // In exploration mode, write only feature.yaml (no SDLC spec artifacts). const templates = - mode === 'fast' - ? TEMPLATES.filter( - ({ filename }) => filename === 'feature.yaml' || filename === 'spec.yaml' - ) - : TEMPLATES; + mode === 'exploration' + ? TEMPLATES.filter(({ filename }) => filename === 'feature.yaml') + : mode === 'fast' + ? TEMPLATES.filter( + ({ filename }) => filename === 'feature.yaml' || filename === 'spec.yaml' + ) + : TEMPLATES; // Write template files await Promise.all( diff --git a/specs/082-prototype-exploration-mode/evidence/app-create-drawer-explore-selected.png b/specs/082-prototype-exploration-mode/evidence/app-create-drawer-explore-selected.png new file mode 100644 index 000000000..fde15b5fe Binary files /dev/null and b/specs/082-prototype-exploration-mode/evidence/app-create-drawer-explore-selected.png differ diff --git a/specs/082-prototype-exploration-mode/evidence/app-create-drawer-mode-selector.png b/specs/082-prototype-exploration-mode/evidence/app-create-drawer-mode-selector.png new file mode 100644 index 000000000..8c66cdee4 Binary files /dev/null and b/specs/082-prototype-exploration-mode/evidence/app-create-drawer-mode-selector.png differ diff --git a/specs/082-prototype-exploration-mode/evidence/app-dashboard-canvas.png b/specs/082-prototype-exploration-mode/evidence/app-dashboard-canvas.png new file mode 100644 index 000000000..b6307ee0b Binary files /dev/null and b/specs/082-prototype-exploration-mode/evidence/app-dashboard-canvas.png differ diff --git a/specs/082-prototype-exploration-mode/evidence/app-settings-page.png b/specs/082-prototype-exploration-mode/evidence/app-settings-page.png new file mode 100644 index 000000000..e24b3c16d Binary files /dev/null and b/specs/082-prototype-exploration-mode/evidence/app-settings-page.png differ diff --git a/specs/082-prototype-exploration-mode/evidence/app-settings-workflow.png b/specs/082-prototype-exploration-mode/evidence/app-settings-workflow.png new file mode 100644 index 000000000..5714d67c8 Binary files /dev/null and b/specs/082-prototype-exploration-mode/evidence/app-settings-workflow.png differ diff --git a/specs/082-prototype-exploration-mode/evidence/build-output.txt b/specs/082-prototype-exploration-mode/evidence/build-output.txt new file mode 100644 index 000000000..2c7142c94 --- /dev/null +++ b/specs/082-prototype-exploration-mode/evidence/build-output.txt @@ -0,0 +1,23 @@ +Build Output Evidence +===================== +Date: 2026-04-02T14:50:00Z + +Command: pnpm build + +> @shepai/cli@1.163.0 build +> pnpm build:cli + +> @shepai/cli@1.163.0 build:cli +> tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json --resolve-full-paths && shx mkdir -p dist/packages/core/src/infrastructure/services/tool-installer && shx rm -rf dist/packages/core/src/infrastructure/services/tool-installer/tools && shx cp -r packages/core/src/infrastructure/services/tool-installer/tools dist/packages/core/src/infrastructure/services/tool-installer/tools && shx rm -rf dist/translations && shx cp -r translations dist/translations + +Exit code: 0 + +Build completed successfully with zero TypeScript errors. +The build includes all exploration mode changes: + - FeatureMode enum (Regular, Fast, Exploration) + - SdlcLifecycle.Exploring state + - Feature iteration fields (iterationCount, maxIterations) + - Exploration agent graph and nodes + - CLI commands (feedback, promote, --explore) + - Web UI components (mode selector, prototype tab, canvas nodes) + - Settings integration (defaultMode, explorationMaxIterations) diff --git a/specs/082-prototype-exploration-mode/evidence/cli-feat-help.txt b/specs/082-prototype-exploration-mode/evidence/cli-feat-help.txt new file mode 100644 index 000000000..88c32ca0c --- /dev/null +++ b/specs/082-prototype-exploration-mode/evidence/cli-feat-help.txt @@ -0,0 +1,83 @@ +=== shep feat --help === +Usage: shep feat [options] [command] + +Manage features through the SDLC lifecycle + +Options: + -h, --help display help for command + +Commands: + new [options] Create a new feature + ls [options] List features + show Show feature details + del [options] Delete a feature + resume Resume a stopped or failed feature agent + start Start a pending feature (spawn the agent) + review [id] Interactive review of a feature waiting for + approval + approve [id] Approve a feature waiting for review + reject [options] [id] Reject a feature waiting for review + logs [options] View feature agent logs + adopt [options] Adopt an existing branch as a tracked feature + archive [options] Archive a feature to hide it from the canvas + unarchive Restore an archived feature to its previous state + feedback Send feedback on an exploration prototype to + iterate + promote [options] Promote an exploration feature to Regular or Fast + mode + help [command] display help for command + +=== shep feat new --help === +Usage: shep feat new [options] + +Create a new feature + +Arguments: + description Feature description + +Options: + -r, --repo Repository path (defaults to current directory) + --push Push branch to remote after implementation + --pr Open PR on implementation complete (implies --push) + --no-pr Do not open PR on implementation complete + --allow-prd Auto-approve through requirements, pause after + --allow-plan Auto-approve through planning, pause at implementation + --allow-merge Auto-approve merge phase + --allow-all Run fully autonomous (no approval pauses) + --parent Parent feature ID (full or partial prefix) + --pending Create feature without starting the agent + --fast Skip SDLC phases and implement directly from your prompt + (default: on) + --no-fast Run full SDLC phases (analyze → requirements → plan → + implement) + --explore Start an exploration/prototyping session for iterative + design + --model LLM model identifier for this run (e.g. claude-opus-4-6) + --no-rebase Skip syncing main from remote before creating the feature + branch + --attach Attach a file (repeatable) (default: []) + -h, --help display help for command + +=== shep feat feedback --help === +Usage: shep feat feedback [options] + +Send feedback on an exploration prototype to iterate + +Arguments: + id Feature ID or prefix + feedback Feedback text for the next iteration + +Options: + -h, --help display help for command + +=== shep feat promote --help === +Usage: shep feat promote [options] + +Promote an exploration feature to Regular or Fast mode + +Arguments: + id Feature ID or prefix + +Options: + --fast Promote to Fast mode (skip SDLC phases) + -h, --help display help for command diff --git a/specs/082-prototype-exploration-mode/evidence/exploration-core-tests.txt b/specs/082-prototype-exploration-mode/evidence/exploration-core-tests.txt new file mode 100644 index 000000000..00444ae99 --- /dev/null +++ b/specs/082-prototype-exploration-mode/evidence/exploration-core-tests.txt @@ -0,0 +1,336 @@ + + RUN  v4.0.18 /Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode + + ✓  node  tests/unit/infrastructure/persistence/sqlite/mappers/feature.mapper.test.ts (35 tests) 22ms + ✓  node  tests/unit/application/use-cases/features/promote-exploration.use-case.test.ts (16 tests) 33ms + ✓  node  tests/unit/application/use-cases/features/delete-feature.use-case.test.ts (40 tests) 86ms + ✓  node  tests/unit/application/use-cases/features/create-feature.use-case.test.ts (55 tests) 45ms + ✓  node  tests/unit/domain/generated/feature-mode-enum.test.ts (10 tests) 5ms + ✓  node  tests/unit/domain/lifecycle-gates.test.ts (12 tests) 5ms +[2026-04-02T11:43:44.656Z] [prototype-generate] Starting prototype generation (iteration 1/10) +[2026-04-02T11:43:44.662Z] [prototype-generate] Executing agent at cwd=/test/repo +[2026-04-02T11:43:44.662Z] [prototype-generate] Prompt length: 1438 chars +[2026-04-02T11:43:44.662Z] [prototype-generate] Prototype generated (19 chars, 0.0s) +[2026-04-02T11:43:44.662Z] [prototype-generate] Interrupting for feedback (iteration 1/10) +[2026-04-02T11:43:44.671Z] [prototype-generate] Starting prototype generation (iteration 1/10) +[2026-04-02T11:43:44.671Z] [prototype-generate] Executing agent at cwd=/test/repo +[2026-04-02T11:43:44.671Z] [prototype-generate] Prompt length: 1438 chars +[2026-04-02T11:43:44.671Z] [prototype-generate] Prototype generated (19 chars, 0.0s) +[2026-04-02T11:43:44.671Z] [prototype-generate] Interrupting for feedback (iteration 1/10) +[2026-04-02T11:43:44.681Z] [prototype-generate] Starting prototype generation (iteration 1/10) +[2026-04-02T11:43:44.685Z] [prototype-generate] Executing agent at cwd=/test/worktree +[2026-04-02T11:43:44.685Z] [prototype-generate] Prompt length: 1442 chars +[2026-04-02T11:43:44.685Z] [prototype-generate] Prototype generated (24 chars, 0.0s) +[2026-04-02T11:43:44.685Z] [prototype-generate] Interrupting for feedback (iteration 1/10) +[2026-04-02T11:43:44.686Z] [prototype-generate] Starting prototype generation (iteration 1/10) +[2026-04-02T11:43:44.687Z] [prototype-generate] Executing agent at cwd=/test/worktree +[2026-04-02T11:43:44.687Z] [prototype-generate] Prompt length: 1442 chars +[2026-04-02T11:43:44.687Z] [prototype-generate] Prototype generated (24 chars, 0.0s) +[2026-04-02T11:43:44.687Z] [prototype-generate] Interrupting for feedback (iteration 1/10) +[2026-04-02T11:43:44.688Z] [prototype-generate] Starting prototype generation (iteration 11/10) +[2026-04-02T11:43:44.688Z] [prototype-generate] Max iterations (10) reached — interrupting for final action +[2026-04-02T11:43:44.689Z] [prototype-generate] Starting prototype generation (iteration 1/10) +[2026-04-02T11:43:44.689Z] [prototype-generate] Executing agent at cwd=/test/worktree +[2026-04-02T11:43:44.689Z] [prototype-generate] Prompt length: 1442 chars +[2026-04-02T11:43:44.690Z] [prototype-generate] Starting prototype generation (iteration 3/10) +[2026-04-02T11:43:44.690Z] [prototype-generate] Executing agent at cwd=/test/worktree +[2026-04-02T11:43:44.690Z] [prototype-generate] Prompt length: 1703 chars +[2026-04-02T11:43:44.690Z] [prototype-generate] Prototype generated (24 chars, 0.0s) +[2026-04-02T11:43:44.690Z] [prototype-generate] Interrupting for feedback (iteration 3/10) +[2026-04-02T11:43:44.690Z] [prototype-generate] Starting prototype generation (iteration 1/10) +[2026-04-02T11:43:44.690Z] [prototype-generate] Executing agent at cwd=/custom/worktree +[2026-04-02T11:43:44.690Z] [prototype-generate] Prompt length: 1444 chars +[2026-04-02T11:43:44.690Z] [prototype-generate] Prototype generated (24 chars, 0.0s) +[2026-04-02T11:43:44.690Z] [prototype-generate] Interrupting for feedback (iteration 1/10) +[2026-04-02T11:43:44.685Z] [prototype-generate] Starting prototype generation (iteration 1/10) +[2026-04-02T11:43:44.685Z] [prototype-generate] Resumed with feedback — routing to apply-feedback +[2026-04-02T11:43:44.688Z] [apply-feedback] Processing user feedback +[2026-04-02T11:43:44.688Z] [apply-feedback] Feedback received (12 chars) for iteration 0 +[2026-04-02T11:43:44.688Z] [apply-feedback] Context prepared for next iteration +[2026-04-02T11:43:44.690Z] [prototype-generate] Starting prototype generation (iteration 1/10) +[2026-04-02T11:43:44.691Z] [prototype-generate] Executing agent at cwd=/test/repo +[2026-04-02T11:43:44.691Z] [prototype-generate] Prompt length: 1525 chars +[2026-04-02T11:43:44.691Z] [prototype-generate] Prototype generated (19 chars, 0.0s) +[2026-04-02T11:43:44.691Z] [prototype-generate] Interrupting for feedback (iteration 1/10) +[2026-04-02T11:43:44.685Z] [prototype-generate] ERROR: Called interrupt() outside the context of a graph. (after 0.0s) +[2026-04-02T11:43:44.687Z] [prototype-generate] ERROR: Called interrupt() outside the context of a graph. (after 0.0s) +[2026-04-02T11:43:44.689Z] [prototype-generate] ERROR: Process exited with code 1 (after 0.0s) +[2026-04-02T11:43:44.690Z] [prototype-generate] ERROR: Called interrupt() outside the context of a graph. (after 0.0s) +[2026-04-02T11:43:44.690Z] [prototype-generate] ERROR: Called interrupt() outside the context of a graph. (after 0.0s) + ✓  node  tests/unit/infrastructure/services/agents/feature-agent/nodes/prototype-generate.node.test.ts (6 tests) 12ms +[2026-04-02T11:43:44.695Z] [prototype-generate] Starting prototype generation (iteration 1/10) +[2026-04-02T11:43:44.696Z] [prototype-generate] Executing agent at cwd=/test/repo +[2026-04-02T11:43:44.696Z] [prototype-generate] Prompt length: 1438 chars +[2026-04-02T11:43:44.696Z] [prototype-generate] Prototype generated (19 chars, 0.0s) +[2026-04-02T11:43:44.696Z] [prototype-generate] Interrupting for feedback (iteration 1/10) +[2026-04-02T11:43:44.701Z] [prototype-generate] Starting prototype generation (iteration 1/10) +[2026-04-02T11:43:44.701Z] [prototype-generate] Resumed with promote/discard — routing to END + ✓  node  tests/unit/infrastructure/services/agents/feature-agent/state.test.ts (29 tests) 9ms +[2026-04-02T11:43:44.712Z] [prototype-generate] Starting prototype generation (iteration 1/10) +[2026-04-02T11:43:44.713Z] [prototype-generate] Executing agent at cwd=/test/repo +[2026-04-02T11:43:44.713Z] [prototype-generate] Prompt length: 1438 chars +[2026-04-02T11:43:44.713Z] [prototype-generate] Prototype generated (19 chars, 0.0s) +[2026-04-02T11:43:44.713Z] [prototype-generate] Interrupting for feedback (iteration 1/10) +[2026-04-02T11:43:44.718Z] [prototype-generate] Starting prototype generation (iteration 1/10) +[2026-04-02T11:43:44.718Z] [prototype-generate] Resumed with promote/discard — routing to END +[2026-04-02T11:43:44.721Z] [prototype-generate] Starting prototype generation (iteration 1/10) +[2026-04-02T11:43:44.722Z] [prototype-generate] Executing agent at cwd=/test/repo +[2026-04-02T11:43:44.722Z] [prototype-generate] Prompt length: 1438 chars +[2026-04-02T11:43:44.722Z] [prototype-generate] Prototype generated (19 chars, 0.0s) +[2026-04-02T11:43:44.722Z] [prototype-generate] Interrupting for feedback (iteration 1/10) + ✓  node  tests/unit/infrastructure/services/agents/langgraph/exploration-agent-graph.test.ts (17 tests) 99ms +[2026-04-02T11:43:44.734Z] [apply-feedback] Processing user feedback +[2026-04-02T11:43:44.735Z] [apply-feedback] Feedback received (12 chars) for iteration 1 +[2026-04-02T11:43:44.735Z] [apply-feedback] Context prepared for next iteration +[2026-04-02T11:43:44.737Z] [apply-feedback] Processing user feedback +[2026-04-02T11:43:44.737Z] [apply-feedback] Feedback received (17 chars) for iteration 1 +[2026-04-02T11:43:44.737Z] [apply-feedback] Context prepared for next iteration +[2026-04-02T11:43:44.737Z] [apply-feedback] Processing user feedback +[2026-04-02T11:43:44.737Z] [apply-feedback] No feedback text provided — proceeding with empty feedback +[2026-04-02T11:43:44.737Z] [apply-feedback] Context prepared for next iteration +[2026-04-02T11:43:44.738Z] [apply-feedback] Processing user feedback +[2026-04-02T11:43:44.738Z] [apply-feedback] No feedback text provided — proceeding with empty feedback +[2026-04-02T11:43:44.738Z] [apply-feedback] Context prepared for next iteration +[2026-04-02T11:43:44.738Z] [apply-feedback] Processing user feedback +[2026-04-02T11:43:44.738Z] [apply-feedback] Feedback received (13 chars) for iteration 2 +[2026-04-02T11:43:44.738Z] [apply-feedback] Context prepared for next iteration +[2026-04-02T11:43:44.738Z] [apply-feedback] Processing user feedback +[2026-04-02T11:43:44.738Z] [apply-feedback] Feedback received (13 chars) for iteration 1 +[2026-04-02T11:43:44.738Z] [apply-feedback] Context prepared for next iteration +[2026-04-02T11:43:44.738Z] [apply-feedback] Processing user feedback +[2026-04-02T11:43:44.738Z] [apply-feedback] Feedback received (4 chars) for iteration 1 +[2026-04-02T11:43:44.738Z] [apply-feedback] Context prepared for next iteration +[2026-04-02T11:43:44.739Z] [apply-feedback] Processing user feedback +[2026-04-02T11:43:44.739Z] [apply-feedback] Feedback received (24 chars) for iteration 2 +[2026-04-02T11:43:44.739Z] [apply-feedback] Context prepared for next iteration + ✓  node  tests/unit/infrastructure/services/agents/feature-agent/nodes/apply-feedback.node.test.ts (8 tests) 7ms +[2026-04-02T11:43:44.770Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.772Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs +[2026-04-02T11:43:44.772Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.772Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.773Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.773Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.773Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.774Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.774Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.774Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.775Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.775Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs +[2026-04-02T11:43:44.775Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.775Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.775Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.775Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.775Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.775Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.775Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.775Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.777Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.777Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs --thread-id thread-abc +[2026-04-02T11:43:44.777Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.777Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.777Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.777Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/thread-abc.db (thread: thread-abc) +[2026-04-02T11:43:44.777Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.777Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.777Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.777Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.778Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.778Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs +[2026-04-02T11:43:44.778Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.778Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.778Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.778Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.778Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.778Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.778Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.778Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.778Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.778Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs +[2026-04-02T11:43:44.778Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.778Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.778Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.778Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.778Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.778Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.778Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.778Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.779Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.779Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs +[2026-04-02T11:43:44.779Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.779Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.779Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.779Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.779Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.779Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.779Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.779Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.779Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.779Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs --worktree-path /wt/path --thread-id thread-abc +[2026-04-02T11:43:44.779Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.779Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.779Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.779Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/thread-abc.db (thread: thread-abc) +[2026-04-02T11:43:44.779Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.779Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.779Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.779Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.780Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.780Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs +[2026-04-02T11:43:44.780Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.780Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.780Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.780Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.780Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.780Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.780Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.780Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.781Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.781Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs +[2026-04-02T11:43:44.781Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.781Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.781Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.781Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.781Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.781Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.781Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.781Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.781Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.781Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs +[2026-04-02T11:43:44.781Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.781Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.781Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.781Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.781Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.781Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.781Z] [default] [WORKER] Graph invocation error: Graph execution failed +[2026-04-02T11:43:44.781Z] [default] [WORKER] Run marked as failed +[2026-04-02T11:43:44.782Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.782Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs --approval-gates {"allowPrd":false,"allowPlan":false,"allowMerge":false} +[2026-04-02T11:43:44.782Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.782Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.782Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.782Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.782Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.782Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.782Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.782Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.782Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.782Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs --approval-gates {"allowPrd":false,"allowPlan":false,"allowMerge":false} +[2026-04-02T11:43:44.782Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.782Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.782Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.782Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.782Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.782Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.782Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.782Z] [default] [WORKER] Run paused — waiting for human approval +[2026-04-02T11:43:44.782Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.783Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs --resume --thread-id thread-abc --resume-from-interrupt +[2026-04-02T11:43:44.783Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/thread-abc.db (thread: thread-abc) +[2026-04-02T11:43:44.783Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Resuming graph from interrupt checkpoint... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.783Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.783Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.783Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs --resume --thread-id thread-abc +[2026-04-02T11:43:44.783Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/thread-abc.db (thread: thread-abc) +[2026-04-02T11:43:44.783Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Resuming graph from error checkpoint... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.783Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.783Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.783Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs +[2026-04-02T11:43:44.783Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.783Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Graph invocation completed. Error: ENOENT: no such file or directory +[2026-04-02T11:43:44.783Z] [default] [WORKER] Run marked as failed: ENOENT: no such file or directory +[2026-04-02T11:43:44.783Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.783Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs --mode Fast +[2026-04-02T11:43:44.783Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.783Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.784Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.784Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.784Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.784Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs +[2026-04-02T11:43:44.784Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.784Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.784Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.784Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.784Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs +[2026-04-02T11:43:44.784Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.784Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.784Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.784Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.784Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs --worktree-path /wt/path --mode Fast +[2026-04-02T11:43:44.784Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.784Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.784Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.784Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.785Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.785Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs --mode Fast +[2026-04-02T11:43:44.785Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.785Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.785Z] [default] [WORKER] Run marked as completed +[2026-04-02T11:43:44.785Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.785Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs --mode Exploration +[2026-04-02T11:43:44.785Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.785Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.785Z] [default] [WORKER] Run paused — waiting for human approval +[2026-04-02T11:43:44.785Z] [default] [WORKER] Starting worker — full command: +[2026-04-02T11:43:44.785Z] [default] [WORKER] feature-agent-worker --feature-id feat-1 --run-id run-1 --repo /repo --spec-dir /specs --mode Exploration +[2026-04-02T11:43:44.785Z] [default] [WORKER] Initializing container... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Loading settings... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Creating executor from configured agent settings... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Creating checkpointer at /Users/arielshadkhan/.shep/checkpoints/run-1.db (thread: run-1) +[2026-04-02T11:43:44.785Z] [default] [WORKER] Updating status to running (PID 31647)... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Starting graph invocation... +[2026-04-02T11:43:44.785Z] [default] [WORKER] Graph invocation completed. Error: none +[2026-04-02T11:43:44.785Z] [default] [WORKER] Run paused — waiting for human approval + ✓  node  tests/unit/infrastructure/services/agents/feature-agent-worker.test.ts (35 tests) 21ms +[2026-04-02T11:43:44.791Z] [default] [WORKER] Received SIGTERM, shutting down... + ✓  node  tests/unit/infrastructure/services/agents/feature-agent/parse-worker-args.test.ts (18 tests) 4ms + + Test Files  12 passed (12) + Tests  281 passed (281) + Start at  14:43:43 + Duration  1.84s (transform 2.32s, setup 464ms, import 5.20s, tests 346ms, environment 1ms) + +[2026-04-02T11:43:45.650Z] [WORKER] Received SIGTERM, shutting down... diff --git a/specs/082-prototype-exploration-mode/evidence/exploration-ui-cli-tests.txt b/specs/082-prototype-exploration-mode/evidence/exploration-ui-cli-tests.txt new file mode 100644 index 000000000..f3e8375bb --- /dev/null +++ b/specs/082-prototype-exploration-mode/evidence/exploration-ui-cli-tests.txt @@ -0,0 +1,22 @@ + + RUN  v4.0.18 /Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode + + ✓  node  tests/unit/infrastructure/services/agents/feature-agent/nodes/prompts/apply-feedback.prompt.test.ts (5 tests) 3ms + ✓  node  tests/unit/domain/factories/settings-defaults.factory.test.ts (30 tests) 7ms + ✓  node  tests/unit/presentation/cli/commands/feat/promote.command.test.ts (7 tests) 17ms + ✓  node  tests/unit/infrastructure/services/agents/feature-agent/nodes/prompts/prototype-generate.prompt.test.ts (15 tests) 7ms + ✓  node  tests/unit/presentation/cli/commands/feat/feedback.command.test.ts (7 tests) 15ms + ✓  web  tests/unit/presentation/web/components/common/control-center-drawer/drawer-view.test.ts (10 tests) 6ms + ✓  web  tests/unit/presentation/web/common/feature-node/derive-feature-state.test.ts (47 tests) 8ms + ✓  web  tests/unit/presentation/web/actions/features/create-feature.test.ts (21 tests) 11ms + ✓  web  tests/unit/presentation/web/components/common/feature-create-drawer/feature-create-drawer.test.tsx (92 tests) 10222ms + ✓ accepts text in the description field  322ms + ✓ sends all-true approvalGates when all checkboxes are checked  393ms + ✓ clears all form fields after submit without unmounting  472ms + ✓ can submit feature after adding new repo via combobox  311ms + + Test Files  9 passed (9) + Tests  234 passed (234) + Start at  14:43:47 + Duration  12.07s (transform 1.69s, setup 1.46s, import 2.42s, tests 10.30s, environment 2.65s) + diff --git a/specs/082-prototype-exploration-mode/evidence/storybook-feature-node-default.png b/specs/082-prototype-exploration-mode/evidence/storybook-feature-node-default.png new file mode 100644 index 000000000..500ae7007 Binary files /dev/null and b/specs/082-prototype-exploration-mode/evidence/storybook-feature-node-default.png differ diff --git a/specs/082-prototype-exploration-mode/evidence/storybook-feature-node-highlight-exploration.png b/specs/082-prototype-exploration-mode/evidence/storybook-feature-node-highlight-exploration.png new file mode 100644 index 000000000..658222c18 Binary files /dev/null and b/specs/082-prototype-exploration-mode/evidence/storybook-feature-node-highlight-exploration.png differ diff --git a/specs/082-prototype-exploration-mode/evidence/storybook-mode-selector-exploration.png b/specs/082-prototype-exploration-mode/evidence/storybook-mode-selector-exploration.png new file mode 100644 index 000000000..b361841ee Binary files /dev/null and b/specs/082-prototype-exploration-mode/evidence/storybook-mode-selector-exploration.png differ diff --git a/specs/082-prototype-exploration-mode/evidence/storybook-prototype-tab-first-iteration.png b/specs/082-prototype-exploration-mode/evidence/storybook-prototype-tab-first-iteration.png new file mode 100644 index 000000000..170bf9248 Binary files /dev/null and b/specs/082-prototype-exploration-mode/evidence/storybook-prototype-tab-first-iteration.png differ diff --git a/specs/082-prototype-exploration-mode/evidence/storybook-prototype-tab-mid-iteration.png b/specs/082-prototype-exploration-mode/evidence/storybook-prototype-tab-mid-iteration.png new file mode 100644 index 000000000..24274a5fa Binary files /dev/null and b/specs/082-prototype-exploration-mode/evidence/storybook-prototype-tab-mid-iteration.png differ diff --git a/specs/082-prototype-exploration-mode/evidence/typespec-generated-types.txt b/specs/082-prototype-exploration-mode/evidence/typespec-generated-types.txt new file mode 100644 index 000000000..34d339530 --- /dev/null +++ b/specs/082-prototype-exploration-mode/evidence/typespec-generated-types.txt @@ -0,0 +1,42 @@ +TypeSpec Generated Types Evidence +================================= +File: packages/core/src/domain/generated/output.ts +Generated by: pnpm tsp:compile + +1. SdlcLifecycle Enum +-------------------------------------- +828:export enum SdlcLifecycle { +export enum SdlcLifecycle { + Started = 'Started', + Analyze = 'Analyze', + Requirements = 'Requirements', + Research = 'Research', + Planning = 'Planning', + Implementation = 'Implementation', + Review = 'Review', + Maintain = 'Maintain', + Blocked = 'Blocked', + Pending = 'Pending', + Exploring = 'Exploring', + Deleting = 'Deleting', + AwaitingUpstream = 'AwaitingUpstream', + Archived = 'Archived', +} + +2. FeatureMode Enum +------------------------------------ +844:export enum FeatureMode { +export enum FeatureMode { + Regular = 'Regular', + Fast = 'Fast', + Exploration = 'Exploration', +} + +3. Feature Type - mode field +----------------------------------------- +1043: mode: FeatureMode; + +4. Feature Type - iteration fields +----------------------------------------------------- +1083: iterationCount: number; +1087: maxIterations?: number; diff --git a/specs/082-prototype-exploration-mode/evidence/unit-test-results.txt b/specs/082-prototype-exploration-mode/evidence/unit-test-results.txt new file mode 100644 index 000000000..44edbebd0 --- /dev/null +++ b/specs/082-prototype-exploration-mode/evidence/unit-test-results.txt @@ -0,0 +1,500 @@ +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/components/common/task-progress-view/task-progress-view.test.tsx (16 tests) 587ms +stderr | tests/unit/presentation/web/components/ui/radix-rtl.test.tsx > Radix UI components in RTL mode > AlertDialog > uses logical text-start on alert dialog header +`AlertDialogContent` requires a description for the component to be accessible for screen reader users. + +You can add a description to the `AlertDialogContent` by passing a `AlertDialogDescription` component as a child, which also benefits sighted users by adding visible context to the dialog. + +Alternatively, you can use your own component as a description by assigning it an `id` and passing the same value to the `aria-describedby` prop in `AlertDialogContent`. If the description is confusing or duplicative for sighted users, you can use the `@radix-ui/react-visually-hidden` primitive as a wrapper around your description component. + +For more information, see https://radix-ui.com/primitives/docs/components/alert-dialog +Warning: Missing `Description` or `aria-describedby={undefined}` for {AlertDialogContent}. + + ✓  web  tests/unit/presentation/web/common/version-badge.test.tsx (8 tests) 562ms + ✓ shows upgrade button in tooltip when update is available  430ms +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/components/ui/radix-rtl.test.tsx (8 tests) 564ms +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/components/common/tech-decisions-review/tech-decisions-review.test.tsx (13 tests) 711ms + ✓  web  tests/unit/presentation/web/components/common/server-log-viewer/server-log-viewer.test.tsx (12 tests) 556ms + ✓  web  tests/unit/presentation/web/hooks/use-cli-upgrade.test.ts (10 tests) 439ms + ✓  web  tests/unit/presentation/web/components/common/prd-questionnaire/prd-questionnaire-sound.test.tsx (4 tests) 488ms + ✓  web  tests/unit/presentation/web/button.test.tsx (8 tests) 376ms + ✓  web  tests/unit/presentation/web/components/features/settings/agent-settings-section.test.tsx (7 tests) 483ms +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/components/common/feature-drawer-tabs/activity-tab.test.tsx (35 tests) 490ms + ✓  web  tests/unit/presentation/web/features/skills/category-filter.test.tsx (10 tests) 493ms + ✓  web  tests/unit/presentation/web/common/floating-action-button.test.tsx (10 tests) 521ms +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/components/common/feature-drawer-tabs/branch-sync-status.test.tsx (12 tests) 381ms + ✓  web  tests/unit/presentation/web/components/features/settings/environment-settings-section.test.tsx (7 tests) 528ms +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/common/feature-node/feature-sessions-dropdown.test.tsx (7 tests) 407ms +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/features/control-center/use-control-center-state.test.tsx (36 tests) 229ms + ✓  web  tests/unit/presentation/web/hooks/use-npm-version-check.test.ts (6 tests) 302ms + ✓  web  tests/unit/presentation/web/components/common/sidebar-collapse-toggle/sidebar-collapse-toggle.test.tsx (2 tests) 267ms + ✓  web  tests/unit/presentation/web/features/version-page-client.test.tsx (2 tests) 316ms + ✓  web  tests/unit/presentation/web/components/common/deployment-status-badge/deployment-status-badge.test.tsx (13 tests) 349ms + ✓  web  tests/unit/presentation/web/components/common/theme-toggle/theme-toggle.test.tsx (7 tests) 479ms + ✓  web  tests/unit/presentation/web/features/skills/skill-list.test.tsx (7 tests) 420ms + ✓ renders category headings for groups with skills  300ms + ✓  web  tests/unit/presentation/web/hooks/use-deployment-logs.test.ts (15 tests) 354ms + ✓  web  tests/unit/presentation/web/layouts/dashboard-layout.test.tsx (6 tests) 339ms + ✓  web  tests/unit/presentation/web/common/empty-state.test.tsx (9 tests) 354ms + ✓  web  tests/unit/presentation/web/components/common/feature-drawer-tabs/plan-tab.test.tsx (13 tests) 278ms + ✓  web  tests/unit/presentation/web/components/ui/sidebar-persistence.test.tsx (9 tests) 393ms +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/components/features/settings/notification-settings-section.test.tsx (6 tests) 317ms +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/components/common/action-button/action-button.test.tsx (14 tests) 457ms + ✓  web  tests/unit/presentation/web/common/feature-list-item.test.tsx (5 tests) 334ms + ✓  web  tests/unit/presentation/web/input.test.tsx (7 tests) 254ms + ✓  web  tests/unit/presentation/web/components/common/action-button/action-button-sound.test.tsx (2 tests) 254ms + ✓  web  tests/unit/presentation/web/common/page-header.test.tsx (8 tests) 433ms +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/layouts/header.test.tsx (5 tests) 264ms +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/components/features/control-center/control-center-integration.test.tsx (15 tests) 392ms + ✓  web  tests/unit/presentation/web/features/skills/skill-card.test.tsx (16 tests) 290ms + ✓  web  tests/unit/presentation/web/components/common/attachment-card/attachment-card.test.tsx (3 tests) 322ms +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/features/control-center/control-center.test.tsx (3 tests) 434ms + ✓  web  tests/unit/presentation/web/card.test.tsx (7 tests) 259ms + ✓  web  tests/unit/presentation/web/alert.test.tsx (4 tests) 230ms + ✓  web  tests/unit/presentation/web/layouts/sidebar.test.tsx (5 tests) 282ms + ✓  web  tests/unit/presentation/web/common/repository-node/repository-node.test.tsx (7 tests) 266ms + ✓  web  tests/unit/presentation/web/components/common/inline-attachments/inline-attachments.test.tsx (13 tests) 123ms +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +stderr | tests/unit/presentation/web/lib/skills.test.ts > getSkills > skips skills with malformed frontmatter +Skipping skill "bad-skill": invalid or missing frontmatter in SKILL.md + + ✓  web  tests/unit/presentation/web/lib/skills.test.ts (48 tests) 116ms +Not implemented: navigation to another Document + ✓  web  tests/unit/presentation/web/components/common/sidebar-nav-item/sidebar-nav-item.test.tsx (1 test) 254ms +Not implemented: HTMLMediaElement's pause() method +stderr | tests/unit/presentation/web/hooks/use-deploy-action.test.ts > useDeployAction > deploy() > sets deployError when server action returns error +[useDeployAction] deploy failed: No dev script found + +stderr | tests/unit/presentation/web/hooks/use-deploy-action.test.ts > useDeployAction > deploy() > persists deployError until retry clears it +[useDeployAction] deploy failed: No dev script found + +stderr | tests/unit/presentation/web/hooks/use-deploy-action.test.ts > useDeployAction > deploy() > catches thrown errors and sets deployError +[useDeployAction] deploy threw exception: Network error Error: Network error + at /Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/tests/unit/presentation/web/hooks/use-deploy-action.test.ts:147:43 + at file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:145:11 + at file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:915:26 + at file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:1243:20 + at new Promise () + at runWithTimeout (file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:1209:10) + at file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:1653:37 + at Traces.$ (file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/vitest@4.0.18_@types+node@25.2.0_jiti@2.6.1_jsdom@28.0.0_lightningcss@1.30.2_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vitest/dist/chunks/traces.CCmnQaNT.js:142:27) + at trace (file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/vitest@4.0.18_@types+node@25.2.0_jiti@2.6.1_jsdom@28.0.0_lightningcss@1.30.2_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vitest/dist/chunks/test.B8ej_ZHS.js:239:21) + at runTest (file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:1653:12) + +stderr | tests/unit/presentation/web/hooks/use-deploy-action.test.ts > useDeployAction > deploy() > is no-op when input is null +[useDeployAction] deploy() called but input is null — no-op + +stderr | tests/unit/presentation/web/hooks/use-deploy-action.test.ts > useDeployAction > deploy() > is no-op while deployLoading is true +[useDeployAction] deploy() called but already loading — no-op + +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/common/sidebar-nav-item.test.tsx (4 tests) 357ms +stderr | tests/unit/presentation/web/hooks/use-deploy-action.test.ts > useDeployAction > polling > does not start polling when deploy fails +[useDeployAction] deploy failed: No dev script + +stderr | tests/unit/presentation/web/hooks/use-deploy-action.test.ts > useDeployAction > cleanup > cleans up error timer on unmount +[useDeployAction] deploy failed: Failed + + ✓  web  tests/unit/presentation/web/hooks/use-deploy-action.test.ts (19 tests) 130ms + ✓  web  tests/unit/presentation/web/components/features/settings/feature-flags-settings-section.test.tsx (4 tests) 180ms + ✓  web  tests/unit/presentation/web/components/common/merge-review/diff-view.test.tsx (13 tests) 218ms + ✓  web  tests/unit/presentation/web/hooks/use-graph-state.test.tsx (24 tests) 174ms + ✓  web  tests/unit/presentation/web/common/feature-status-badges.test.tsx (5 tests) 120ms + ✓  web  tests/unit/presentation/web/components/common/feature-drawer/use-feature-actions.test.ts (24 tests) 104ms + ✓  web  tests/unit/presentation/web/components/common/product-decisions-summary/product-decisions-summary.test.tsx (9 tests) 131ms +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/components/common/ci-status-badge/ci-status-badge.test.tsx (3 tests) 53ms + ✓  web  tests/unit/presentation/web/app-layout-integration.test.tsx (4 tests) 168ms + ✓  web  tests/unit/presentation/web/components/features/settings/database-settings-section.test.tsx (4 tests) 102ms + ✓  web  tests/unit/presentation/web/api/tools-install.test.ts (4 tests) 56ms + ✓  web  tests/unit/presentation/web/hooks/use-version.test.ts (4 tests) 73ms + ✓  web  tests/unit/presentation/web/hooks/use-viewport-persistence.test.ts (21 tests) 97ms + ✓  web  tests/unit/presentation/web/api/deployment-logs-sse.test.ts (10 tests) 44ms + ✓  web  tests/unit/presentation/web/features/control-center/control-center-empty-state.test.tsx (3 tests) 120ms + ✓  web  tests/unit/presentation/web/badge.test.tsx (3 tests) 57ms + ✓  web  tests/unit/presentation/web/hooks/use-agent-events.test.ts (13 tests) 53ms + ✓  web  tests/unit/presentation/web/components/common/tech-decisions-review/tech-decisions-review-sound.test.tsx (2 tests) 106ms + ✓  web  tests/unit/presentation/web/common/feature-status-group.test.tsx (3 tests) 99ms + ✓  web  tests/unit/presentation/web/api/directory-list.test.ts (16 tests) 78ms + ✓  web  tests/unit/presentation/web/common/sidebar-section-header.test.tsx (3 tests) 98ms +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's play() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method +Not implemented: HTMLMediaElement's pause() method + ✓  web  tests/unit/presentation/web/hooks/use-notifications.test.ts (9 tests) 107ms + ✓  web  tests/unit/presentation/web/lib/rtl-fonts.test.ts (7 tests) 85ms + ✓  web  tests/unit/presentation/web/components/common/feature-drawer-tabs/use-tab-data-fetch.test.ts (16 tests) 106ms + ✓  web  tests/unit/presentation/web/common/elapsed-time.test.tsx (5 tests) 91ms + ✓  web  tests/unit/presentation/web/use-theme.test.tsx (6 tests) 25ms + ✓  web  tests/unit/presentation/web/app/build-graph-nodes.test.ts (18 tests) 14ms + ✓  web  tests/unit/presentation/web/api/tools-install-stream.test.ts (4 tests) 49ms + ✓  web  tests/unit/presentation/web/components/common/repository-node/use-repository-actions.test.ts (15 tests) 54ms + ✓  web  tests/unit/presentation/web/hooks/use-drawer-sync.test.ts (11 tests) 107ms + ✓  web  tests/unit/presentation/web/common/loading-skeleton.test.tsx (9 tests) 83ms + ✓  web  tests/unit/presentation/web/hooks/use-sound-action.test.ts (28 tests) 37ms + ✓  web  tests/unit/presentation/web/i18n.test.tsx (7 tests) 68ms + ✓  web  tests/unit/presentation/web/actions/merge-review/get-merge-review-data.test.ts (23 tests) 44ms +stderr | tests/unit/presentation/web/actions/deploy-feature.test.ts > deployFeature server action > returns error when featureId is empty +[deployFeature] rejected — featureId is empty + +stderr | tests/unit/presentation/web/actions/deploy-feature.test.ts > deployFeature server action > returns error when feature is not found +[deployFeature] feature not found in repository: "nonexistent-id" + +stderr | tests/unit/presentation/web/actions/deploy-feature.test.ts > deployFeature server action > returns error when worktree directory does not exist +[deployFeature] worktree path does not exist on disk: "/mock/.shep/repos/abc123/wt/feat-my-feature" + +stderr | tests/unit/presentation/web/actions/deploy-feature.test.ts > deployFeature server action > returns error when service.start throws +[deployFeature] error: No dev script found in package.json Error: No dev script found in package.json + at Object. (/Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/tests/unit/presentation/web/actions/deploy-feature.test.ts:97:13) + at Object.Mock [as start] (file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/@vitest+spy@4.0.18/node_modules/@vitest/spy/dist/index.js:285:34) + at deployFeature (/Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/src/presentation/web/app/actions/deploy-feature.ts:55:23) + at processTicksAndRejections (node:internal/process/task_queues:105:5) + at /Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/tests/unit/presentation/web/actions/deploy-feature.test.ts:100:20 + at file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:915:20 + +stderr | tests/unit/presentation/web/actions/deploy-feature.test.ts > deployFeature server action > returns generic error for non-Error throws +[deployFeature] error: Failed to deploy feature unexpected + + ✓  web  tests/unit/presentation/web/actions/deploy-feature.test.ts (6 tests) 14ms + ✓  web  tests/unit/presentation/web/hooks/use-tool-install-stream.test.ts (6 tests) 57ms +stderr | tests/unit/presentation/web/actions/deploy-repository.test.ts > deployRepository server action > validates repositoryPath is an absolute path +[deployRepository] rejected — not an absolute path + +stderr | tests/unit/presentation/web/actions/deploy-repository.test.ts > deployRepository server action > returns error for empty repositoryPath +[deployRepository] rejected — not an absolute path + +stderr | tests/unit/presentation/web/actions/deploy-repository.test.ts > deployRepository server action > returns error when directory does not exist +[deployRepository] directory does not exist: "/nonexistent/path" + +stderr | tests/unit/presentation/web/actions/deploy-repository.test.ts > deployRepository server action > returns error when service.start throws +[deployRepository] error: No dev script found in package.json Error: No dev script found in package.json + at Object. (/Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/tests/unit/presentation/web/actions/deploy-repository.test.ts:98:13) + at Object.Mock [as start] (file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/@vitest+spy@4.0.18/node_modules/@vitest/spy/dist/index.js:285:34) + at deployRepository (/Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/src/presentation/web/app/actions/deploy-repository.ts:39:23) + at /Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/tests/unit/presentation/web/actions/deploy-repository.test.ts:101:26 + at file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:145:11 + at file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:915:26 + at file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:1243:20 + at new Promise () + at runWithTimeout (file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:1209:10) + at file:///Users/arielshadkhan/.shep/repos/fbfd7efb528913ed/wt/feat-prototype-exploration-mode/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:1653:37 + +stderr | tests/unit/presentation/web/actions/deploy-repository.test.ts > deployRepository server action > returns generic error for non-Error throws +[deployRepository] error: Failed to deploy repository unexpected + + ✓  web  tests/unit/presentation/web/actions/deploy-repository.test.ts (7 tests) 11ms + ✓  web  tests/unit/presentation/web/smoke-imports.test.ts (5 tests) 25ms + ✓  web  tests/unit/presentation/web/hooks/sidebar-features-context.test.tsx (11 tests) 52ms + ✓  web  tests/unit/presentation/web/actions/update-settings.test.ts (6 tests) 10ms + ✓  web  tests/unit/presentation/web/actions/open-folder.test.ts (12 tests) 7ms + ✓  web  tests/unit/presentation/web/actions/open-ide.test.ts (9 tests) 7ms + ✓  web  tests/unit/presentation/web/api/version.test.ts (4 tests) 13ms + ✓  web  tests/unit/presentation/web/actions/get-feature-drawer-data.test.ts (7 tests) 7ms + ✓  web  tests/unit/presentation/web/actions/features/create-feature.test.ts (21 tests) 13ms + ✓  web  tests/unit/presentation/web/actions/update-model.test.ts (8 tests) 7ms + ✓  web  tests/unit/presentation/web/actions/open-shell.test.ts (17 tests) 13ms + ✓  web  tests/unit/presentation/web/lib/language.test.ts (11 tests) 5ms + ✓  web  tests/unit/presentation/web/app/actions/list-github-repositories.test.ts (7 tests) 6ms + ✓  web  tests/unit/presentation/web/common/feature-node/derive-feature-state.test.ts (47 tests) 8ms + ✓  web  tests/unit/presentation/web/actions/reject-feature.test.ts (12 tests) 5ms + ✓  web  tests/unit/presentation/web/lib/feature-flags.test.ts (14 tests) 13ms + ✓  web  tests/unit/presentation/web/app/actions/get-feature-phase-timings.test.ts (8 tests) 6ms + ✓  web  tests/unit/presentation/web/actions/load-settings.test.ts (3 tests) 7ms + ✓  web  tests/unit/presentation/web/actions/get-viewer-permission.test.ts (8 tests) 6ms + ✓  web  tests/unit/presentation/web/actions/features/delete-feature.test.ts (8 tests) 6ms + ✓  web  tests/unit/presentation/web/actions/get-deployment-status.test.ts (5 tests) 5ms + ✓  web  tests/unit/presentation/web/lib/parse-log-line.test.ts (25 tests) 8ms + ✓  web  tests/unit/presentation/web/actions/get-supported-models.test.ts (6 tests) 7ms + ✓  web  tests/unit/presentation/web/actions/stop-deployment.test.ts (5 tests) 4ms + ✓  web  tests/unit/presentation/web/actions/approve-feature.test.ts (7 tests) 37ms + ✓  web  tests/unit/presentation/web/actions/get-workflow-defaults.test.ts (4 tests) 4ms + ✓  web  tests/unit/presentation/web/app/actions/get-feature-plan.test.ts (8 tests) 6ms + ✓  web  tests/unit/presentation/web/lib/derive-graph.test.ts (15 tests) 10ms + ✓  web  tests/unit/presentation/web/actions/pick-files.test.ts (4 tests) 5ms + ✓  web  tests/unit/presentation/web/components/common/merge-review/merge-review-config.test.ts (11 tests) 6ms + ✓  web  tests/unit/presentation/web/actions/get-deployment-logs.test.ts (5 tests) 3ms + ✓  web  tests/unit/presentation/web/features/control-center/derive-state.test.ts (6 tests) 3ms + ✓  web  tests/unit/presentation/web/app/actions/list-github-organizations.test.ts (4 tests) 4ms + ✓  web  tests/unit/presentation/web/actions/compose-user-input.test.ts (7 tests) 3ms + ✓  web  tests/unit/presentation/web/lib/format-duration.test.ts (9 tests) 5ms + ✓  web  tests/unit/presentation/web/app/actions/import-github-repository.test.ts (9 tests) 7ms + ✓  web  tests/unit/presentation/web/components/common/control-center-drawer/drawer-view.test.ts (10 tests) 5ms + ✓  web  tests/unit/presentation/web/actions/pick-folder.test.ts (4 tests) 4ms + ✓  web  tests/unit/presentation/web/lib/compare-versions.test.ts (7 tests) 3ms + ✓  web  tests/unit/presentation/web/components/common/feature-create-drawer/feature-create-drawer.test.tsx (92 tests) 26940ms + ✓ renders the drawer header when open  309ms + ✓ accepts text in the description field  779ms + ✓ enables submit button when description has content  632ms + ✓ calls onSubmit with payload containing description (no name field)  784ms + ✓ includes attachments array with file objects  555ms + ✓ sends approvalGates with only PRD checked  878ms + ✓ sends all-true approvalGates when all checkboxes are checked  1048ms + ✓ sends all-false approvalGates when no checkboxes are checked  712ms + ✓ resets all switches to defaults after close and reopen  300ms + ✓ submits correct approvalGates after All toggle  653ms + ✓ clears all form fields after submit without unmounting  1518ms + ✓ clears form data on submit so next open starts fresh  706ms + ✓ supports multiple file selections across multiple picks  434ms + ✓ removes attachment when remove button is clicked  426ms + ✓ does not add duplicate files when the same file is picked again  330ms + ✓ plays create sound on form submit  550ms + ✓ submitting with fast mode selected includes mode=Fast in payload  704ms + ✓ submitting with regular mode selected includes mode=Regular in payload  559ms + ✓ submitting with exploration mode selected includes mode=Exploration in payload  741ms + ✓ mode resets to default after submit  663ms + ✓ submits the form when Ctrl+Enter is pressed in the textarea  384ms + ✓ submits the form when Meta+Enter is pressed in the textarea  586ms + ✓ does not submit on plain Enter (without modifier)  496ms + ✓ includes sessionId in submitted payload  844ms + ✓ submit button is disabled when no repo selected and repositoryPath is empty  669ms + ✓ submit button is enabled when repo is selected via combobox  795ms + ✓ submit button is enabled when repositoryPath is provided (canvas flow)  363ms + ✓ handleSubmit includes selectedRepoPath in payload  946ms + ✓ filters repositories by path when typing in search input  347ms + + Test Files  383 passed (383) + Tests  5459 passed (5459) + Start at  14:41:49 + Duration  38.49s (transform 11.92s, setup 42.58s, import 63.29s, tests 98.94s, environment 81.26s) + diff --git a/specs/082-prototype-exploration-mode/feature.yaml b/specs/082-prototype-exploration-mode/feature.yaml new file mode 100644 index 000000000..0abd8fa4a --- /dev/null +++ b/specs/082-prototype-exploration-mode/feature.yaml @@ -0,0 +1,44 @@ +feature: + id: "082-prototype-exploration-mode" + name: "prototype-exploration-mode" + number: 82 + branch: "feat/082-prototype-exploration-mode" + lifecycle: "research" + createdAt: "2026-04-02T07:16:41Z" +status: + phase: "implementation-complete" + progress: + completed: 29 + total: 29 + percentage: 100 + currentTask: null + lastUpdated: "2026-04-03T08:58:34.309Z" + lastUpdatedBy: "feature-agent:implement" + completedPhases: + - "analyze" + - "requirements" + - "research" + - "plan" + - "phase-1" + - "phase-2" + - "phase-3" + - "phase-4" + - "phase-5" + - "phase-6" + - "phase-7" + - "evidence" +validation: + lastRun: null + gatesPassed: [] + autoFixesApplied: [] +tasks: + current: null + blocked: [] + failed: [] +checkpoints: + - phase: "feature-created" + completedAt: "2026-04-02T07:16:41Z" + completedBy: "feature-agent" +errors: + current: null + history: [] diff --git a/specs/082-prototype-exploration-mode/plan.yaml b/specs/082-prototype-exploration-mode/plan.yaml new file mode 100644 index 000000000..2593c40ed --- /dev/null +++ b/specs/082-prototype-exploration-mode/plan.yaml @@ -0,0 +1,232 @@ +name: "prototype-exploration-mode" +summary: > + Implementation plan for adding exploration/prototyping mode to Shep. The approach replaces + the boolean fast field with a FeatureMode enum (Regular, Fast, Exploration), adds an Exploring + lifecycle state, creates a new LangGraph exploration graph factory with interrupt-based feedback + loops, extends the Web UI with a three-way mode selector and prototype review drawer, adds CLI + commands for feedback and promotion, and performs an in-place database migration. The plan is + organized into 7 phases: domain foundation, persistence migration, agent graph infrastructure, + application use cases, CLI presentation, Web UI presentation, and settings integration. + +relatedFeatures: + - "052-fast-prompt-to-pr" + +technologies: + - "TypeScript" + - "TypeSpec" + - "LangGraph (@langchain/langgraph)" + - "React / Next.js" + - "Commander.js" + - "shadcn/ui" + - "SQLite (better-sqlite3)" + - "tsyringe (DI)" + - "React Flow (@xyflow/react)" + - "Lucide React (icons)" + - "Storybook" + - "Vitest" + +relatedLinks: + - title: "LangGraph Interrupt/Resume Documentation" + url: "https://langchain-ai.github.io/langgraphjs/how-tos/human_in_the_loop/breakpoints/" + - title: "LangGraph Annotation State Channels" + url: "https://langchain-ai.github.io/langgraphjs/concepts/low_level/" + - title: "TypeSpec Enum Documentation" + url: "https://typespec.io/docs/language-basics/enums" + +phases: + - id: "phase-1" + name: "Domain Foundation (TypeSpec + Generated Code)" + description: "Define FeatureMode enum and Exploring lifecycle state in TypeSpec, add iteration fields to Feature entity, compile to TypeScript. This phase is the prerequisite for all other work since every layer depends on these domain types." + parallel: false + + - id: "phase-2" + name: "Persistence Migration (Boolean-to-Enum + Schema)" + description: "Create SQLite migration to replace the boolean fast column with a TEXT mode column, update the feature mapper from boolean to enum mapping, and update repository queries. This phase must follow domain types and precede any runtime code that reads mode." + parallel: false + + - id: "phase-3" + name: "Agent Graph Infrastructure (Exploration Graph + Worker Routing)" + description: "Build the exploration agent graph factory with prototype-generate and apply-feedback nodes, extend FeatureAgentAnnotation with exploration state channels, create the exploration prompt builder, and update worker routing from --fast boolean to --mode enum. This is the core runtime engine for exploration mode." + parallel: false + + - id: "phase-4" + name: "Application Use Cases (Create + Promote + Discard)" + description: "Extend CreateFeatureUseCase to handle exploration mode (Exploring lifecycle, minimal spec, exploration graph spawning), create PromoteExplorationUseCase for in-place mode transition, and extend discard/delete logic for exploration cleanup." + parallel: false + + - id: "phase-5" + name: "CLI Presentation (Commands + Flags)" + description: "Add --explore flag to feat new, create feat feedback and feat promote subcommands, update workflow defaults from boolean fast to mode enum. CLI is addressed before Web UI because it is thinner and validates the use case API surface." + parallel: false + + - id: "phase-6" + name: "Web UI Presentation (Mode Selector + Canvas + Drawer)" + description: "Replace the fast toggle with a three-way mode selector (ToggleGroup), add exploring state config for canvas nodes with amber styling and FlaskConical icon, create prototype review drawer tab with diff view and feedback input, and add promote/discard actions." + parallel: false + + - id: "phase-7" + name: "Settings Integration + Polish" + description: "Replace workflow.defaultFastMode boolean with workflow.defaultMode enum in TypeSpec and settings service, add explorationMaxIterations setting, wire defaults into CLI and Web UI, and perform end-to-end integration testing." + parallel: false + +filesToCreate: + - "tsp/common/enums/feature-mode.tsp" + - "packages/core/src/infrastructure/persistence/sqlite/migrations/051-replace-fast-with-mode.ts" + - "packages/core/src/infrastructure/services/agents/feature-agent/exploration-agent-graph.ts" + - "packages/core/src/infrastructure/services/agents/feature-agent/exploration-agent-graph.test.ts" + - "packages/core/src/infrastructure/services/agents/feature-agent/nodes/prototype-generate.node.ts" + - "packages/core/src/infrastructure/services/agents/feature-agent/nodes/prototype-generate.node.test.ts" + - "packages/core/src/infrastructure/services/agents/feature-agent/nodes/apply-feedback.node.ts" + - "packages/core/src/infrastructure/services/agents/feature-agent/nodes/apply-feedback.node.test.ts" + - "packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/prototype-generate.prompt.ts" + - "packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/apply-feedback.prompt.ts" + - "packages/core/src/application/use-cases/features/promote/promote-exploration.use-case.ts" + - "packages/core/src/application/use-cases/features/promote/promote-exploration.use-case.test.ts" + - "packages/core/src/presentation/cli/commands/feat/feedback.command.ts" + - "packages/core/src/presentation/cli/commands/feat/promote.command.ts" + - "packages/core/src/presentation/web/components/common/feature-drawer-tabs/tabs/prototype-tab.tsx" + - "packages/core/src/presentation/web/components/common/feature-drawer-tabs/tabs/prototype-tab.stories.tsx" + - "packages/core/src/presentation/web/components/common/feature-create-drawer/mode-selector.tsx" + - "packages/core/src/presentation/web/components/common/feature-create-drawer/mode-selector.stories.tsx" + +filesToModify: + - "tsp/domain/entities/feature.tsp" + - "tsp/common/enums/lifecycle.tsp" + - "tsp/domain/entities/settings.tsp" + - "packages/core/src/domain/generated/output.ts" + - "packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.ts" + - "packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.test.ts" + - "packages/core/src/infrastructure/persistence/sqlite/migrations/index.ts" + - "packages/core/src/infrastructure/services/agents/feature-agent/state.ts" + - "packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.ts" + - "packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.ts" + - "packages/core/src/infrastructure/services/spec/spec-initializer.service.ts" + - "packages/core/src/application/use-cases/features/create/create-feature.use-case.ts" + - "packages/core/src/application/use-cases/features/create/create-feature.use-case.test.ts" + - "packages/core/src/presentation/cli/commands/feat/new.command.ts" + - "packages/core/src/presentation/cli/commands/feat/index.ts" + - "packages/core/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.tsx" + - "packages/core/src/presentation/web/components/common/feature-node/feature-node-state-config.ts" + - "packages/core/src/presentation/web/components/common/feature-node/feature-node.tsx" + - "packages/core/src/presentation/web/components/common/feature-drawer-tabs/feature-drawer-tabs.tsx" + - "packages/core/src/infrastructure/services/settings/settings-defaults.factory.ts" + +openQuestions: [] + +content: | + ## Architecture Overview + + Exploration mode is a third workflow mode alongside Regular (full SDLC) and Fast (direct + implementation). It follows the same architectural patterns already established for fast mode: + a dedicated LangGraph graph factory, worker routing based on a mode flag, and presentation-layer + toggles that map to a domain enum. + + The key architectural insight is that exploration differs from fast mode in exactly two ways: + (1) graph topology — exploration loops via interrupt/resume for feedback instead of running + a single pass, and (2) lifecycle state — exploration uses a dedicated Exploring state instead + of jumping straight to Implementation. Everything else — worktree isolation, agent execution, + canvas rendering, feature entity storage — is reused as-is. + + The implementation layers out cleanly across Clean Architecture: + - **Domain (TypeSpec)**: FeatureMode enum, Exploring lifecycle state, iteration fields + - **Application**: Extended CreateFeatureUseCase, new PromoteExplorationUseCase + - **Infrastructure**: Exploration graph factory, exploration nodes, worker routing, migration + - **Presentation**: CLI flags/commands, Web UI mode selector and prototype drawer tab + + ## Key Design Decisions + + ### 1. FeatureMode Enum Replacing Boolean fast Field + + The boolean `fast` field on Feature is replaced with a `FeatureMode` enum + (`Regular`, `Fast`, `Exploration`). This prevents the invalid state space that two booleans + would create (fast=true AND exploration=true) and scales cleanly to future modes. The enum + is defined in TypeSpec and auto-generates to TypeScript. The database migration maps + `fast=1` to `'Fast'` and `fast=0` to `'Regular'`. + + **Trade-off**: This is a cross-cutting migration that touches mapper, repository, use cases, + CLI, and Web UI. However, it is a one-time cost that pays for itself in domain correctness + and prevents an entire category of bugs. + + ### 2. Exploring Lifecycle State + + A dedicated `Exploring` state is added to `SdlcLifecycle` rather than reusing `Started` or + `Implementation`. The lifecycle enum is used directly by canvas node styling, drawer tab + visibility, SSE event mapping, and worker lifecycle updates. A dedicated state ensures all + these systems handle exploration correctly without special-case boolean checks. + + Valid transitions from Exploring: + - `Exploring -> Implementation` (promote to fast — skip SDLC) + - `Exploring -> Requirements` (promote to regular — full SDLC from requirements) + - `Exploring -> Deleting` (discard exploration) + - Self-loop for feedback iterations (lifecycle stays Exploring) + + ### 3. New Graph Factory with Looping Topology + + A third graph factory `createExplorationAgentGraph()` is created alongside the existing + `createFeatureAgentGraph()` and `createFastFeatureAgentGraph()`. The topology is: + `START -> prototype-generate -> interrupt(feedback) -> [apply-feedback -> prototype-generate]* -> END`. + + The graph uses custom node functions rather than the `executeNode()` factory because + `executeNode()` is tightly coupled to the approval gate pattern (completedPhases, + shouldInterrupt, _approvalAction). The exploration nodes reuse lower-level shared utilities + (retryExecute, createNodeLogger, recordPhaseStart/End) for clean separation. + + ### 4. Interrupt/Resume for Feedback Loop + + The feedback loop reuses the existing LangGraph `interrupt()` and `Command({resume, update})` + pattern proven by approval gates. The resume payload extends to support three actions: + `{action: 'iterate', feedback: string}`, `{action: 'promote', targetMode: 'Regular'|'Fast'}`, + and `{action: 'discard'}`. This avoids building a new communication channel and preserves + full agent context across iterations via LangGraph checkpointing. + + ### 5. In-Place Mode Transition for Promotion + + Promoting an exploration to a real feature is a state transition, not an entity migration. + The PromoteExplorationUseCase changes mode (Exploration -> Regular or Fast), transitions + lifecycle (Exploring -> Requirements or Implementation), optionally scaffolds missing spec + YAMLs, and spawns the appropriate agent graph. The worktree and prototype code are preserved. + + ### 6. Web UI Mode Selector + + The binary fast-mode Switch toggle is replaced with a three-option SegmentedControl + (shadcn/ui ToggleGroup with type='single'). Each option has an icon (ClipboardList, Zap, + FlaskConical) and label. When Explore is selected, approval gates are hidden entirely. + + ### 7. Iteration Count on Feature Entity + + The iteration count is persisted on the Feature entity (not just in graph state) so the + Web UI can read it through the standard feature data flow without accessing checkpoint DBs. + The graph nodes update the feature record after each iteration. + + ## Implementation Strategy + + The phases are ordered by dependency: + + 1. **Domain first** — TypeSpec enums and entity fields must exist before any code references them. + 2. **Persistence second** — The database schema must match the new domain model before runtime + code can read/write features. + 3. **Agent graph third** — The exploration graph factory and nodes must exist before use cases + can spawn them. + 4. **Use cases fourth** — Create/Promote/Discard use cases depend on domain types, persistence, + and graph infrastructure. + 5. **CLI fifth** — CLI commands are thin wrappers around use cases; implementing them validates + the use case API. + 6. **Web UI sixth** — Web UI is the richest presentation layer; it builds on the same use cases + with additional visual components. + 7. **Settings last** — Settings integration wires defaults into both CLI and Web UI, making it + a natural final phase. + + Within each phase, work proceeds in TDD cycles: write a failing test that asserts the new + behavior, implement the minimum code to pass, then refactor for quality. + + ## Risk Mitigation + + | Risk | Mitigation | + | ---- | ---------- | + | Boolean-to-enum migration breaks existing features | Migration maps fast=1 to Fast and fast=0 to Regular with explicit verification. Integration test confirms existing features continue to work post-migration. | + | Exploration graph feedback loop runs indefinitely | Configurable iteration limit (default 10) in settings. Graph notifies user when approaching limit and forces promote/discard at max. | + | Context window degradation over many iterations | Prompt builder summarizes older feedback history instead of including full text. Only the latest 3 iterations get full context; earlier rounds get single-line summaries. | + | Checkpoint database grows with iterations | Each checkpoint is ~10-50KB. Even 20+ iterations produce negligible impact. Discarded explorations trigger checkpoint cleanup. | + | Worker crash during feedback loop loses state | LangGraph checkpointing persists state after each node. Resume-from-interrupt recovers from the last checkpoint automatically. | + | ToggleGroup mode selector confuses existing users | Default mode matches existing behavior (Fast if defaultFastMode was true). The switch-to-ToggleGroup change preserves the user's current default. | + | Parallel development risk from XL feature size | Phases are independent enough to be developed sequentially with clear boundaries. Each phase has its own test suite that validates before moving on. | diff --git a/specs/082-prototype-exploration-mode/research.yaml b/specs/082-prototype-exploration-mode/research.yaml new file mode 100644 index 000000000..c54c1314e --- /dev/null +++ b/specs/082-prototype-exploration-mode/research.yaml @@ -0,0 +1,545 @@ +name: "prototype-exploration-mode" +summary: > + Technical research for adding an exploration/prototyping mode to Shep. Key decisions: + replace boolean fast field with FeatureMode enum (TypeSpec-defined), add Exploring lifecycle + state, create a new LangGraph exploration graph factory with interrupt-based feedback loops, + extend the Web UI with a three-way mode selector and prototype review drawer, and perform + an in-place database migration from INTEGER fast column to TEXT mode column. + +relatedFeatures: + - "052-fast-prompt-to-pr" + +technologies: + - "TypeScript" + - "TypeSpec" + - "LangGraph (@langchain/langgraph)" + - "React / Next.js" + - "Commander.js" + - "shadcn/ui" + - "SQLite (better-sqlite3)" + - "tsyringe (DI)" + - "React Flow (@xyflow/react)" + - "Lucide React (icons)" + - "Storybook" + - "Vitest" + +relatedLinks: + - title: "LangGraph Interrupt/Resume Documentation" + url: "https://langchain-ai.github.io/langgraphjs/how-tos/human_in_the_loop/breakpoints/" + - title: "LangGraph Annotation State Channels" + url: "https://langchain-ai.github.io/langgraphjs/concepts/low_level/" + - title: "TypeSpec Enum Documentation" + url: "https://typespec.io/docs/language-basics/enums" + +decisions: + - title: "FeatureMode Enum Design (TypeSpec)" + chosen: "Three-value TypeSpec enum FeatureMode { Regular, Fast, Exploration } replacing the boolean fast field" + rejected: + - "Additional exploration boolean alongside fast — creates 4 states (2 booleans) but only 3 are valid; impossible combinations (fast=true AND exploration=true) cannot be prevented at the type level; does not scale to future modes" + - "String union type instead of enum — loses TypeSpec's enum benefits (auto-generated code, documentation, validation); less discoverable in IDE tooling" + rationale: | + The codebase currently uses a boolean fast field defined in tsp/domain/entities/feature.tsp + (line 370) and stored as INTEGER (0/1) in SQLite (migration 027). Two booleans would create + four states where only three are valid. A TypeSpec enum is the natural extension — it matches + the existing SdlcLifecycle enum pattern, auto-generates to TypeScript via tsp:compile, and + prevents invalid state combinations at the type level. The enum values (Regular, Fast, + Exploration) are self-documenting and scale to future modes without additional fields. + + - title: "Database Migration Strategy (fast boolean to mode enum)" + chosen: "New migration (051+) that adds TEXT mode column, migrates data from fast INTEGER, then drops fast column using SQLite table recreation" + rejected: + - "Keep fast column and add separate mode column — leaves dead column in schema, creates confusion about which is authoritative, and violates single-source-of-truth principle" + - "Rename fast column to mode and change type in-place — SQLite does not support ALTER COLUMN; would require the same table recreation approach but with more confusing migration semantics" + rationale: | + SQLite does not support ALTER COLUMN TYPE, so the migration must use the standard + create-new-table, copy-data, drop-old, rename pattern. The codebase already has + 35 legacy migrations (legacy-migrations.ts) plus file-based migrations up to 050 + (packages/core/src/infrastructure/persistence/sqlite/migrations/). The new migration + will: (1) add a TEXT mode column with DEFAULT 'Regular', (2) UPDATE mode = 'Fast' + WHERE fast = 1, (3) drop the fast column via table recreation. The mapper + (feature.mapper.ts) will change from boolean mapping (row.fast === 1) to string + mapping (row.mode as FeatureMode). This is a proven pattern in this codebase. + + - title: "Exploring Lifecycle State Placement" + chosen: "Add Exploring to the SdlcLifecycle enum in tsp/common/enums/lifecycle.tsp with defined transition rules" + rejected: + - "Reuse Started state for exploration — conflates two different semantics; Started means 'agent not yet running' while Exploring means 'actively generating prototypes in a feedback loop'; would break existing lifecycle gate logic" + - "Reuse Implementation state with a sub-state flag — adds implicit state that is invisible to lifecycle queries and UI rendering; the canvas and drawer tabs use lifecycle directly for rendering decisions" + rationale: | + The SdlcLifecycle enum (lifecycle.tsp lines 24-126) currently has 13 states. Adding + Exploring follows the established pattern (e.g., AwaitingUpstream was added for fork-and-PR + features). The lifecycle is used directly by: the Web UI canvas for node styling + (feature-node-state-config.ts), the drawer tabs for visibility (feature-drawer-tabs.tsx + computeVisibleTabs), the SSE event route for status mapping (agent-events/route.ts + LIFECYCLE_TO_NODE), and the worker for lifecycle updates (node-helpers.ts + updateNodeLifecycle). A dedicated state ensures all these systems handle exploration + correctly without special-case boolean checks. + + - title: "Exploration Agent Graph Topology" + chosen: "New createExplorationAgentGraph() factory with looping topology: START -> prototype-generate -> interrupt(feedback) -> [apply-feedback -> prototype-generate]* -> END" + rejected: + - "Extend the fast graph with a feedback loop — the fast graph (fast-feature-agent-graph.ts) is intentionally minimal (2 nodes) and designed for single-pass execution; adding loop edges and feedback state would complicate its simplicity and break the separation of concerns" + - "Single monolithic node that handles everything — loses LangGraph's checkpoint benefits; if the agent crashes during feedback application, the entire iteration is lost; separate nodes allow checkpoint resume from the last successful step" + rationale: | + The codebase follows a clear graph factory pattern: createFeatureAgentGraph() for full SDLC + and createFastFeatureAgentGraph() for fast mode (both in infrastructure/services/agents/ + feature-agent/). A third factory createExplorationAgentGraph() follows this pattern exactly. + The topology is simple: prototype-generate (a custom node function) calls the agent executor + to generate code, then interrupts for feedback using the same interrupt() mechanism proven + in approval gates (node-helpers.ts lines 616-628). On resume, apply-feedback prepares + context and routes back to prototype-generate. The loop exits when the user sends a promote + or discard action instead of text feedback. This reuses the existing Command({resume, + update}) resume pattern from the worker (feature-agent-worker.ts lines 329-362). + + - title: "Feedback Loop Interrupt Mechanism" + chosen: "Reuse existing LangGraph interrupt() with Command({resume, update}) pattern, extending the resume payload to include feedback text and action type" + rejected: + - "New agent run per iteration — each feedback round would spawn a new worker process, losing checkpoint state and requiring context reconstruction from scratch; the codebase's checkpoint-first design (checkpointer at ~/.shep/checkpoints/{threadId}.db) makes interrupt/resume the natural choice" + - "Polling-based feedback (agent polls a queue/file for user input) — introduces unnecessary complexity, race conditions, and a new communication channel when interrupt/resume already provides exactly this capability" + rationale: | + The interrupt/resume pattern is battle-tested in this codebase for approval gates. + The worker already handles resume-from-interrupt (feature-agent-worker.ts lines 329-362) + by parsing --resume-payload JSON and deriving state updates via Command({resume, update}). + For exploration, the resume payload extends from {approved: true} | {rejected: true, + feedback: string} to include {feedback: string, action: 'iterate'} | + {action: 'promote', targetMode: 'Regular' | 'Fast'} | {action: 'discard'}. The state + update channels (_approvalAction, _rejectionFeedback) are extended with + exploration-specific channels (iterationCount, feedbackHistory, explorationStatus). + This is a minimal, safe extension of proven infrastructure. + + - title: "Exploration State Channels (FeatureAgentAnnotation Extension)" + chosen: "Add exploration-specific channels to FeatureAgentAnnotation: iterationCount (number), maxIterations (number), feedbackHistory (string[] with accumulating reducer), explorationStatus (union type)" + rejected: + - "Separate ExplorationAgentAnnotation — LangGraph requires the same annotation type across graphs that share checkpointing infrastructure; the worker casts between graph types already (feature-agent-worker.ts line 287); a separate annotation would break the cast pattern" + - "Store feedback in the existing messages channel — messages is an accumulating string array used for node execution logs; mixing feedback history with execution logs makes it impossible to extract feedback for display or context building" + rationale: | + The FeatureAgentAnnotation (state.ts) already uses the extension pattern — it has + channels for CI watch (ciFixAttempts, ciFixHistory, ciFixStatus) that are only used + by the merge node. Adding exploration channels follows the same pattern: they are + only used by the exploration graph nodes but coexist in the shared annotation. + The feedbackHistory channel uses the accumulating reducer pattern [...prev, ...next] + already proven by messages and evidence channels. iterationCount uses the simple + override reducer (_prev, next) => next. explorationStatus uses a union type like + ciFixStatus. The exploration graph nodes read/write these channels; other graphs + ignore them (they default to initial values). + + - title: "Worker Graph Routing (Fast/Regular/Exploration)" + chosen: "Replace boolean --fast flag with --mode flag accepting 'regular' | 'fast' | 'exploration'; worker selects graph factory based on mode value" + rejected: + - "Add separate --exploration flag alongside --fast — creates the same boolean combination problem as the domain model; --fast and --exploration could both be true; does not scale to future modes" + - "Encode mode in the feature record and have worker read from DB — adds a DB dependency to the worker startup path which currently receives all needed info via CLI args; increases latency and failure surface" + rationale: | + The worker currently receives --fast as a boolean flag and selects the graph factory + (feature-agent-worker.ts lines 285-290). Replacing this with --mode 'regular' | 'fast' | + 'exploration' follows the same pattern but supports the enum. The worker's argument + parsing (parseWorkerArgs) changes from a boolean fast check to a string mode value. + The graph selection becomes a switch/if-else chain on mode value. The process service + (feature-agent-process.service.ts) passes --mode instead of --fast. The CreateFeatureUseCase + already passes fast to the spawner; it changes to passing mode. This is a clean, + localized change. + + - title: "Web UI Mode Selector Design" + chosen: "Replace Switch toggle with a three-option SegmentedControl (ToggleGroup) using shadcn/ui ToggleGroup" + rejected: + - "Keep Switch and add a second toggle for Explore — two independent toggles create the same invalid-state problem as two booleans; users could enable both" + - "Dropdown/Select component — adds an extra click (open dropdown, then select); a segmented control shows all options at once and is more discoverable for three mutually exclusive options" + rationale: | + The current fast mode UI is a Switch toggle (feature-create-drawer.tsx lines 825-844) + with a Zap icon. For three mutually exclusive modes, a SegmentedControl (implemented + via shadcn/ui ToggleGroup with type='single') is the standard pattern. Each option + shows an icon (ClipboardList for Regular, Zap for Fast, FlaskConical for Explore) + and a label. When Explore is selected, approval gates are hidden entirely (not just + disabled like in fast mode) since exploration has no SDLC gates. The ToggleGroup + component from shadcn/ui is already available in the project and provides the right + behavior (single selection, accessible, keyboard navigable). + + - title: "Canvas Node Visual Treatment for Exploration Features" + chosen: "Extend feature-node-state-config.ts with an 'exploring' state entry using amber/yellow color scheme and FlaskConical icon, plus an iteration count badge" + rejected: + - "Separate canvas section/tab for exploration features — fragments the user's workspace; users should see all work in one place; requires significant React Flow layout changes" + - "Same visual treatment as fast mode with just a different icon — exploration features have fundamentally different user interaction (feedback loop vs single-pass); they need to be immediately visually distinguishable from both fast and regular features" + rationale: | + The feature-node-state-config.ts (lines 297-388) already defines state-based styling + with icon, borderClass, labelClass, badgeClass, and label per state. The fast mode badge + uses Zap icon (feature-node.tsx line 331). For exploration, an 'exploring' state with + amber/yellow border (border-s-amber-400), FlaskConical icon from lucide-react, and a + badge showing iteration count follows the established pattern. The feature-node.tsx + already conditionally renders the fast mode Zap icon (lines 329-341); extending this + to show FlaskConical for exploration mode is a minimal change. + + - title: "Prototype Review in Feature Drawer" + chosen: "Add a 'Prototype' tab to the feature drawer tabs visible only when lifecycle is Exploring, showing diff view + feedback input + promote/discard actions" + rejected: + - "Reuse the existing 'merge-review' tab — merge-review is specifically designed for PR review with diff summary, CI status, and merge action; exploration review needs feedback input, iteration history, and promote/discard which are fundamentally different interactions" + - "Inline feedback in the overview tab — the overview tab already has a dense layout with metadata; adding a feedback form, iteration history, and action buttons would make it cluttered and break the existing layout" + rationale: | + The drawer tab system (feature-drawer-tabs.tsx) dynamically shows tabs based on lifecycle + and state via computeVisibleTabs() (lines 61-94). Adding a 'prototype' tab key that is + visible when lifecycle === 'exploring' follows the exact pattern used for 'prd-review' + (shown when lifecycle === 'requirements' and state === 'action-required') and 'merge-review' + (shown when lifecycle === 'review'). The tab content includes: (1) a code diff view showing + the prototype changes (reusing existing diff rendering infrastructure), (2) a feedback + TextArea with submit button, (3) a promote button opening a mode selection dialog + (Regular or Fast), and (4) a discard button with confirmation dialog. Each iteration + is displayed with its feedback text and timestamp. + + - title: "Promotion Mechanism (Exploration to Regular/Fast)" + chosen: "PromoteExplorationUseCase performs in-place mode transition: update mode field, transition lifecycle, optionally scaffold missing spec YAMLs, spawn appropriate agent graph" + rejected: + - "Create new Feature entity from exploration — requires data migration between entities, loses iteration history, duplicates git worktree/branch management, and makes the promotion flow fragile" + - "Manual user workflow (discard exploration, create new feature, cherry-pick code) — poor UX, high friction, users would lose the exploration context and feedback history" + rationale: | + The Feature entity already has a mode field (after migration) and lifecycle state. + Promotion is a state transition: mode changes from Exploration to Regular or Fast, + lifecycle changes from Exploring to Requirements (regular) or Implementation (fast). + The existing worktree and branch are preserved — the prototype code becomes the + starting point. For promotion to regular mode, the spec initializer can scaffold + the missing YAMLs (research.yaml, plan.yaml, tasks.yaml) since it already supports + mode-based template selection (spec-initializer.service.ts lines 293-300). The new + agent graph (regular or fast) is spawned via the existing process service. This is + the simplest path that preserves all context. + + - title: "CLI Command Design for Exploration" + chosen: "Add --explore flag to 'shep feat new', new 'shep feat feedback' subcommand, and new 'shep feat promote' subcommand" + rejected: + - "Separate 'shep explore' top-level command — breaks the established 'shep feat' command group pattern; exploration is a mode of feature development, not a separate concept; would require duplicating shared feat infrastructure" + - "Interactive TUI-only feedback (no CLI subcommand) — excludes CI/automation use cases and users who prefer CLI over Web UI; the feedback command enables scriptable exploration iteration" + rationale: | + The CLI uses Commander.js with a 'feat' command group (src/presentation/cli/commands/feat/). + The existing pattern has feat new with flags (--fast, --pending, etc.), feat del, feat start, + feat archive, etc. Adding --explore to feat new follows the same flag pattern. The feedback + and promote subcommands follow the pattern of feat start (takes feature-id, performs action). + The new.command.ts already resolves workflow defaults and passes them to CreateFeatureUseCase + (lines 62-83, 163, 174); extending this for --explore is straightforward. + + - title: "Settings Integration (defaultMode)" + chosen: "Add workflow.defaultMode setting (string enum: 'regular' | 'fast' | 'exploration') replacing workflow.defaultFastMode boolean" + rejected: + - "Keep defaultFastMode and add defaultExplorationMode boolean — same invalid-state problem as the feature flag approach; two booleans, four states, three valid" + - "No default mode setting (always require explicit flag) — breaks existing UX where features default to fast mode; users expect a configurable default" + rationale: | + The settings TypeSpec (tsp/domain/entities/settings.tsp lines 283-367) defines + WorkflowConfig with defaultFastMode: boolean = true. This maps to the domain + SettingsDefaults (settings-defaults.factory.ts line 151). Replacing the boolean with + a string enum 'regular' | 'fast' | 'exploration' (with default 'fast' to preserve + backward compatibility) is a clean migration. The settings DB column changes from + INTEGER to TEXT, matching the feature mode migration pattern. The Web UI mode selector + defaults to this preference. The CLI feat new command reads it via getWorkflowDefaults() + (new.command.ts lines 62-83). + + - title: "Spec Initialization for Exploration Mode" + chosen: "Extend spec initializer mode parameter to accept 'exploration', writing only feature.yaml (no spec.yaml, no research/plan/tasks)" + rejected: + - "No spec initialization at all — breaks the convention that all features have a spec directory; the spec directory is used for feature number allocation (resolveNextNumber), phase tracking (getCompletedPhases), and the Web UI spec viewer" + - "Full spec scaffolding like regular mode — creates unused files (research.yaml, plan.yaml, tasks.yaml) that are meaningless for exploration; confusing for users who browse the spec directory" + rationale: | + The spec initializer (spec-initializer.service.ts lines 293-300) already supports + mode-based template filtering: fast mode writes feature.yaml + spec.yaml, regular mode + writes all 5 files. Exploration mode needs even less: only feature.yaml (to store the + exploration prompt and metadata). spec.yaml is not needed because exploration does not + follow the spec-driven workflow. The initialize() interface already accepts mode as an + optional string parameter — extending it from 'fast' | undefined to + 'fast' | 'exploration' | undefined is a minimal change. + + - title: "Exploration Node Implementation Strategy" + chosen: "Custom node functions with shared utilities (retryExecute, createNodeLogger, recordPhaseStart/End) instead of the executeNode() factory" + rejected: + - "Reuse executeNode() factory directly — tightly coupled to approval gate pattern (completedPhases check, shouldInterrupt, _approvalAction handling); exploration has fundamentally different flow semantics (interrupt every iteration, use feedback text, loop instead of route forward)" + - "Fork executeNode() into executeExplorationNode() — creates code duplication; the shared utilities are already individually exported and testable" + rationale: | + executeNode() (node-helpers.ts lines 512-649) is tightly coupled to the approval gate + pattern: it checks completedPhases, evaluates shouldInterrupt(), handles _approvalAction, + and manages _needsReexecution routing. The exploration graph has fundamentally different + flow semantics — it interrupts EVERY iteration (not conditionally based on gates), uses + feedback text instead of approve/reject, and loops instead of routing forward. Custom + node functions that reuse the lower-level utilities (retryExecute for agent calls, + createNodeLogger for logging, recordPhaseStart/End for timing) give the exploration + graph clean semantics without the overhead of gate logic. + +openQuestions: + - question: "Should the exploration graph reuse executeNode() from node-helpers.ts or implement custom node functions?" + resolved: true + options: + - option: "Reuse executeNode() factory" + description: "Use the existing executeNode() factory that handles lifecycle updates, phase tracking, retry logic, and timing. Provides consistent behavior across all graph types but includes approval gate logic that is irrelevant for exploration nodes." + selected: false + - option: "Custom node functions with shared utilities" + description: "Write custom node functions for prototype-generate and apply-feedback that directly call shared utilities (retryExecute, createNodeLogger, recordPhaseStart/End) without the executeNode() wrapper. This avoids the approval gate overhead and allows exploration-specific logic like feedback accumulation." + selected: true + - option: "Fork executeNode() into executeExplorationNode()" + description: "Copy executeNode() and modify it for exploration semantics: remove approval gate logic, add feedback context building, add iteration tracking. Creates code duplication but provides maximum control over the exploration flow." + selected: false + selectionRationale: | + executeNode() (node-helpers.ts lines 512-649) is tightly coupled to the approval gate + pattern: it checks completedPhases, evaluates shouldInterrupt(), handles _approvalAction, + and manages _needsReexecution routing. The exploration graph has fundamentally different + flow semantics — it interrupts EVERY iteration (not conditionally based on gates), uses + feedback text instead of approve/reject, and loops instead of routing forward. Custom + node functions that reuse the lower-level utilities (retryExecute for agent calls, + createNodeLogger for logging, recordPhaseStart/End for timing) give the exploration + graph clean semantics without the overhead of gate logic. The shared utilities are + already exported and well-tested. + + - question: "How should the iteration count be persisted and communicated to the Web UI?" + resolved: true + options: + - option: "Graph state channel only (iterationCount in FeatureAgentAnnotation)" + description: "Store iteration count solely in the LangGraph state. The SSE route reads it from the checkpoint DB or interrupt payload. Simple but requires checkpoint DB access from the web layer." + selected: false + - option: "Feature entity field persisted to database" + description: "Add iterationCount and maxIterations fields to the Feature entity and persist them in SQLite. The exploration graph nodes update the feature record after each iteration. The Web UI reads it from the feature API like any other field." + selected: true + - option: "Interrupt payload metadata" + description: "Include iteration count in the interrupt() payload alongside the feedback prompt. The worker writes it to the agent_runs table result column. The Web UI reads it from the run status. Lightweight but fragile — depends on parsing unstructured run result data." + selected: false + selectionRationale: | + The Web UI reads feature data from the feature repository via API routes. Adding + iterationCount and maxIterations as Feature entity fields follows the existing pattern + for per-feature metadata (push, openPr, ciWatchEnabled are all Feature fields persisted + to SQLite). The exploration graph nodes update these fields via the feature repository + (the merge node already does this — merge.node.ts line 105 calls featureRepository.findById + and update). This ensures the Web UI has real-time access to iteration state through the + standard feature data flow, and it persists across worker restarts/crashes. The graph + state channel also tracks it for the agent's own context, but the DB is the source of + truth for the UI. + + - question: "Should the exploration prompt use a separate prompt builder or extend buildFastImplementPrompt?" + resolved: true + options: + - option: "Separate buildExplorationPrompt function" + description: "Create a dedicated prompt builder that emphasizes quick prototyping, minimal scope, and throwaway quality. Clear separation of concerns, each mode has its own prompt strategy." + selected: true + - option: "Extend buildFastImplementPrompt with mode parameter" + description: "Add a mode parameter to the existing fast prompt builder to vary the instructions. Reduces file count but couples two different prompting strategies in one function." + selected: false + - option: "Generic prompt template with mode-specific sections" + description: "Create a template system where prompts are composed from mode-specific sections. Over-engineered for three modes; the prompt strategies differ significantly enough to warrant separate builders." + selected: false + selectionRationale: | + The fast-implement prompt (nodes/fast-implement.node.ts) is tuned for single-pass + implementation from a clear user request. Exploration prompts have different goals: + generate a quick visual prototype, prioritize showing the concept over production quality, + include iteration context (previous feedback, what to change). These are fundamentally + different prompt strategies. A separate buildExplorationPrompt() function in a new + exploration-prompt.ts file follows the existing pattern where each node type has its own + prompt builder (buildAnalyzePrompt, buildRequirementsPrompt, buildFastImplementPrompt). + The exploration prompt builder takes the additional feedbackHistory from state to include + iteration context. + +content: | + ## Technology Decisions + + ### 1. FeatureMode Enum Design (TypeSpec) + + **Chosen:** Three-value TypeSpec enum `FeatureMode { Regular, Fast, Exploration }` replacing the boolean `fast` field + + **Rejected:** + - Additional exploration boolean alongside fast — creates 4 states but only 3 valid; impossible combinations cannot be prevented at type level + - String union type instead of enum — loses TypeSpec's enum benefits (auto-generated code, documentation, validation) + + **Rationale:** The codebase uses a boolean `fast` field (feature.tsp line 370, SQLite migration 027). Two booleans create an invalid state space. A TypeSpec enum matches the existing SdlcLifecycle pattern, auto-generates to TypeScript, and prevents invalid combinations at the type level. + + ### 2. Database Migration Strategy + + **Chosen:** New migration (051+) adding TEXT `mode` column, migrating data from fast INTEGER, dropping fast column via table recreation + + **Rejected:** + - Keep both columns — dead column creates confusion about source of truth + - Rename in-place — SQLite doesn't support ALTER COLUMN TYPE + + **Rationale:** SQLite requires create-new-table, copy-data, drop-old, rename for column type changes. The codebase has 35 legacy migrations plus file-based migrations up to 050. The migration maps `fast=1` to `'Fast'`, `fast=0` to `'Regular'`, adds `'Exploration'` as a new valid value. Feature mapper changes from boolean to string mapping. + + ### 3. Exploring Lifecycle State + + **Chosen:** Add `Exploring` to `SdlcLifecycle` enum in `tsp/common/enums/lifecycle.tsp` + + **Rejected:** + - Reuse Started state — conflates "not yet running" with "actively prototyping" + - Reuse Implementation with sub-state flag — invisible to lifecycle queries and UI + + **Rationale:** Lifecycle is used directly by canvas nodes, drawer tabs, SSE events, and worker lifecycle updates. A dedicated state ensures all systems handle exploration without boolean checks. Valid transitions: Exploring to Implementation (promote-fast), Exploring to Requirements (promote-regular), Exploring to Deleting (discard), plus self-loop for iterations. + + ### 4. Exploration Agent Graph Topology + + **Chosen:** New `createExplorationAgentGraph()` factory: `START -> prototype-generate -> interrupt -> [apply-feedback -> prototype-generate]* -> END` + + **Rejected:** + - Extend fast graph with loop — fast graph is intentionally minimal (2 nodes); adding loops breaks its simplicity + - Single monolithic node — loses LangGraph checkpoint benefits for crash recovery + + **Rationale:** Follows the established graph factory pattern. The prototype-generate node calls the agent executor, interrupts for feedback using proven interrupt() mechanism, and loops via apply-feedback. Exit via promote/discard action in resume payload. + + ### 5. Feedback Loop Interrupt Mechanism + + **Chosen:** Reuse LangGraph `interrupt()` with `Command({resume, update})`, extending resume payload for feedback + + **Rejected:** + - New agent run per iteration — loses checkpoint state, requires context reconstruction + - Polling-based feedback — unnecessary complexity when interrupt/resume already provides this + + **Rationale:** The worker already handles resume-from-interrupt (feature-agent-worker.ts lines 329-362). Resume payload extends to: `{feedback: string, action: 'iterate'}`, `{action: 'promote', targetMode: 'Regular'|'Fast'}`, `{action: 'discard'}`. State channels extended for exploration context. + + ### 6. Exploration State Channels + + **Chosen:** Add channels to `FeatureAgentAnnotation`: `iterationCount`, `maxIterations`, `feedbackHistory` (accumulating), `explorationStatus` + + **Rejected:** + - Separate annotation type — breaks worker's graph type casting pattern + - Reuse messages channel for feedback — mixes execution logs with feedback + + **Rationale:** The annotation already has mode-specific channels (CI fix channels used only by merge node). Exploration channels follow the same pattern. Accumulating reducer for feedbackHistory matches messages and evidence patterns. Other graphs ignore these channels (default to initial values). + + ### 7. Worker Graph Routing + + **Chosen:** Replace `--fast` boolean flag with `--mode` string flag (`regular`|`fast`|`exploration`) + + **Rejected:** + - Separate --exploration flag — creates same boolean combination problem + - Read mode from DB — adds DB dependency to worker startup + + **Rationale:** Worker currently receives --fast and selects graph (feature-agent-worker.ts lines 285-290). A --mode flag supports the enum cleanly. Process service and CreateFeatureUseCase pass mode instead of fast. Graph selection becomes a switch on mode value. + + ### 8. Web UI Mode Selector + + **Chosen:** Three-option SegmentedControl (ToggleGroup) replacing the Switch toggle + + **Rejected:** + - Two independent Switch toggles — invalid state combinations possible + - Dropdown/Select — extra click for three mutually exclusive options + + **Rationale:** Current Switch toggle (feature-create-drawer.tsx lines 825-844) works for binary choice. For three modes, shadcn/ui ToggleGroup with type='single' shows all options with icons (ClipboardList/Regular, Zap/Fast, FlaskConical/Explore). When Explore is selected, approval gates are hidden entirely. + + ### 9. Canvas Visual Treatment + + **Chosen:** Extend `feature-node-state-config.ts` with 'exploring' state using amber color + FlaskConical icon + iteration badge + + **Rejected:** + - Separate canvas section — fragments workspace, requires major layout changes + - Same treatment as fast mode — exploration has fundamentally different UX + + **Rationale:** State config defines icon/borderClass/label per state. Adding 'exploring' with amber border, FlaskConical icon, and iteration count badge follows the established pattern. + + ### 10. Prototype Review Drawer Tab + + **Chosen:** New 'prototype' tab visible when lifecycle is Exploring + + **Rejected:** + - Reuse merge-review tab — designed for PR review, not exploration feedback + - Inline in overview tab — would make overview cluttered + + **Rationale:** The tab system dynamically shows tabs based on lifecycle via computeVisibleTabs(). Adding 'prototype' when lifecycle is Exploring follows the pattern for 'prd-review' and 'merge-review'. Content: diff view, feedback TextArea, promote button (mode dialog), discard button. + + ### 11. Promotion Mechanism + + **Chosen:** In-place mode transition via PromoteExplorationUseCase + + **Rejected:** + - Create new Feature entity — loses history, duplicates worktree management + - Manual workflow — high friction, loses context + + **Rationale:** Promotion changes mode (Exploration to Regular/Fast) and lifecycle (Exploring to Requirements or Implementation). Worktree and branch preserved. Missing spec YAMLs scaffolded on promotion to regular mode. + + ### 12. CLI Command Design + + **Chosen:** --explore flag on 'feat new', plus 'feat feedback' and 'feat promote' subcommands + + **Rejected:** + - Separate 'shep explore' command — breaks feat command group pattern + - TUI-only feedback — excludes CI/automation use cases + + **Rationale:** Commander.js feat group already has new, del, start, archive. Adding --explore and new subcommands follows the same pattern. + + ### 13. Settings Integration + + **Chosen:** `workflow.defaultMode` string replacing `workflow.defaultFastMode` boolean + + **Rejected:** + - Additional boolean — same invalid-state problem + - No default setting — breaks existing UX + + **Rationale:** Settings TypeSpec defines WorkflowConfig with defaultFastMode boolean. Replacing with defaultMode string ('regular'|'fast'|'exploration', default 'fast') is a clean migration preserving backward compatibility. + + ### 14. Spec Initialization + + **Chosen:** Extend spec initializer mode to accept 'exploration', writing only feature.yaml + + **Rejected:** + - No spec at all — breaks feature number allocation and phase tracking + - Full spec scaffolding — creates meaningless unused files + + **Rationale:** Spec initializer already supports mode-based filtering. Exploration needs only feature.yaml. Interface extends from 'fast' | undefined to 'fast' | 'exploration' | undefined. + + ### 15. Exploration Node Implementation + + **Chosen:** Custom node functions with shared utilities instead of executeNode() factory + + **Rejected:** + - Reuse executeNode() directly — tightly coupled to approval gate pattern + - Fork executeNode() — creates code duplication + + **Rationale:** executeNode() handles completedPhases, shouldInterrupt, _approvalAction, _needsReexecution which are all approval gate concerns. Custom node functions using retryExecute, createNodeLogger, recordPhaseStart/End provide clean exploration semantics. + + ## Library Analysis + + | Library | Purpose | Decision | Reasoning | + | ------- | ------- | -------- | --------- | + | @langchain/langgraph | Graph execution, checkpointing, interrupt/resume | Use (existing) | Core infrastructure already in use; exploration graph uses identical APIs | + | lucide-react (FlaskConical) | Exploration mode icon | Use (existing) | Already used for all icons (Zap, ClipboardList, CircleAlert, etc.) | + | shadcn/ui (ToggleGroup) | Three-way mode selector | Use (existing) | Already in the project; type='single' provides mutually exclusive selection | + | better-sqlite3 | Database migration for mode column | Use (existing) | All migrations use this; table recreation pattern is proven | + | @xyflow/react | Canvas node extension | Use (existing) | React Flow canvas already renders feature nodes; exploration nodes extend existing component | + | vitest | Testing exploration graph, use cases, and components | Use (existing) | All tests in the codebase use vitest | + | No new libraries needed | — | — | All requirements can be met with existing dependencies | + + ## Security Considerations + + - **Worktree isolation**: Exploration prototypes run in isolated git worktrees, same as regular features. No shared state between explorations. The existing IWorktreeService handles creation and cleanup. + - **Agent executor sandboxing**: The exploration agent uses the same IAgentExecutor interface with the same execution constraints (working directory restricted to worktree). No new attack surface. + - **Feedback injection**: User feedback text is passed to the agent as prompt context. The existing agent executor already handles untrusted user input (userQuery). No additional sanitization needed beyond what the agent executor provides. + - **Resource limits**: Each iteration generates a checkpoint. For unbounded iterations (no limit per spec), disk usage grows linearly. Consider checkpoint auto-cleanup for discarded explorations. + - **Promotion authorization**: Same authorization as feature creation. No new authorization model needed. + + ## Performance Implications + + - **Prototype generation speed (NFR-1)**: Exploration prompt prioritizes speed over completeness. Target: initial prototype < 2 minutes, iterations < 90 seconds. + - **Checkpoint growth**: Each iteration creates a checkpoint (~10-50KB). Even 20+ iterations produce negligible disk impact. + - **Web UI responsiveness (NFR-5)**: SSE events provide real-time status updates. The existing 2-second DB polling handles exploration state changes. + - **Context window management**: feedbackHistory accumulates across iterations. The prompt builder should summarize older feedback to stay within token limits for long exploration sessions. + - **Database migration**: fast-to-mode migration runs once per upgrade. For typical installations (< 1000 features), completes in < 1 second. + + ## Architecture Notes + + ### Layer Mapping (Clean Architecture Compliance) + + | Layer | New Components | + | ----- | -------------- | + | Domain (tsp/) | FeatureMode enum, Exploring lifecycle state, iteration fields on Feature | + | Application (use-cases/) | PromoteExplorationUseCase, extended CreateFeatureUseCase | + | Infrastructure (agents/) | createExplorationAgentGraph(), exploration nodes, buildExplorationPrompt(), worker --mode routing | + | Infrastructure (persistence/) | Migration 051+ for mode column, feature mapper update | + | Infrastructure (services/) | Spec initializer 'exploration' mode support | + | Presentation (web/) | Mode selector ToggleGroup, Prototype drawer tab, Exploring canvas node style, feedback/promote/discard actions | + | Presentation (cli/) | --explore flag, feat feedback command, feat promote command | + + ### Implementation Order (Dependencies) + + 1. TypeSpec models (FeatureMode enum, Exploring lifecycle, iteration fields) — everything depends on these + 2. Database migration (fast boolean to mode enum) — mapper and repository changes + 3. Worker routing (--mode flag replacing --fast) — enables graph selection + 4. Exploration graph factory — new graph with prototype-generate, apply-feedback nodes + 5. CreateFeatureUseCase mode support — exploration lifecycle initialization + 6. PromoteExplorationUseCase — mode transition, graph re-spawning + 7. CLI commands (--explore flag, feedback, promote subcommands) + 8. Web UI (mode selector, canvas node, drawer tab, feedback UI) + 9. Settings migration (defaultMode replacing defaultFastMode) + + ### Existing Pattern Reuse Summary + + - Graph factory pattern: identical to createFastFeatureAgentGraph() but different topology + - Interrupt/resume pattern: identical to approval gates but with feedback payload + - State channel pattern: identical to CI fix channels (used by one graph, ignored by others) + - Spec initializer mode: extending existing mode parameter + - Worker flag routing: same pattern as --fast but with string enum + - Canvas node styling: same state config with different colors/icons + - Drawer tab visibility: same computeVisibleTabs() with new lifecycle check + - Use case pattern: same DI-based use case pattern with output port interfaces diff --git a/specs/082-prototype-exploration-mode/spec.yaml b/specs/082-prototype-exploration-mode/spec.yaml new file mode 100644 index 000000000..1cd197c9d --- /dev/null +++ b/specs/082-prototype-exploration-mode/spec.yaml @@ -0,0 +1,129 @@ +name: "prototype-exploration-mode" +number: 82 +branch: "feat/082-prototype-exploration-mode" +oneLiner: "Add a prototype/exploration mode for iterative visual prototyping before committing to full feature development" +summary: "Add a new \"exploration\" mode alongside the existing \"fast\" and \"regular\" (full SDLC) modes. In exploration mode, users describe a vague idea and the system generates quick, disposable prototypes that can be viewed in the Web UI, iterated on with feedback, and eventually promoted to a real feature (fast or regular mode) once the direction is clear. This enables a discovery-first workflow where users explore design space before committing to implementation.\n" +phase: "Requirements" +sizeEstimate: "XL" +relatedFeatures: + - "052-fast-prompt-to-pr" +technologies: + - "TypeScript" + - "TypeSpec" + - "LangGraph" + - "React / Next.js" + - "Commander.js" + - "shadcn/ui" + - "SQLite (better-sqlite3)" + - "tsyringe (DI)" + - "React Flow (@xyflow/react)" +relatedLinks: [] +openQuestions: + - question: "Should the feature mode be modeled as a FeatureMode enum replacing the boolean fast field, or as an additional exploration boolean alongside fast?" + resolved: true + options: + - option: "FeatureMode enum" + description: "Replace the boolean fast field with a FeatureMode enum (Regular, Fast, Exploration). Cleaner domain model, prevents invalid states like fast=true AND exploration=true, and scales to future modes without additional booleans. Requires migration of existing fast boolean to enum." + selected: true + - option: "Additional exploration boolean" + description: "Add an exploration boolean alongside the existing fast boolean. Minimizes migration risk and keeps changes localized. However, creates potential for invalid state combinations and does not scale well if more modes are added later." + selected: false + selectionRationale: "A FeatureMode enum is the cleaner domain model. The boolean fast field is already a code smell — two booleans create four states but only three are valid. An enum makes the mode explicit, self-documenting, and prevents invalid combinations. The migration cost is bounded (one TypeSpec enum + repository column change + UI toggle update) and is a worthwhile investment for correctness." + answer: "FeatureMode enum" + - question: "Should exploration mode reuse the existing Feature entity with a new Exploring lifecycle state, or introduce a separate Prototype entity?" + resolved: true + options: + - option: "Reuse Feature entity with Exploring lifecycle state" + description: "Add an Exploring state to SdlcLifecycle and use the existing Feature entity. Reuses all existing infrastructure (repository, UI, worker process, canvas rendering). Promotion is a state transition rather than entity conversion. Simpler to implement and maintain." + selected: true + - option: "Separate Prototype entity" + description: "Create a new Prototype aggregate root with its own repository, lifecycle, and UI components. Provides full domain separation and avoids Feature entity bloat. However, requires duplicating significant infrastructure (repository, worker, canvas nodes, drawer) and makes promotion complex (data migration between entities)." + selected: false + selectionRationale: "Reusing the Feature entity is the pragmatic choice. Exploration features share the same core behaviors (worktree, agent execution, canvas display) and differ only in graph topology and lifecycle gates. A new entity would duplicate massive infrastructure. The Feature entity already accommodates fast mode via a flag — exploration mode follows the same pattern. Adding an Exploring lifecycle state is a minimal, safe extension." + answer: "Reuse Feature entity with Exploring lifecycle state" + - question: "What should the exploration agent generate as a prototype artifact?" + resolved: true + options: + - option: "Code diff in worktree" + description: "The agent writes actual code changes to an isolated worktree branch, same as fast mode. The user reviews real code diffs and can see the prototype running locally if they choose. Promotion means the code is already in place. Leverages existing worktree infrastructure entirely." + selected: false + - option: "Markdown/HTML preview document" + description: "The agent generates a rich description document (markdown with wireframes, architecture diagrams, API sketches) rather than code. Faster to generate and easier to throw away. But requires a separate rendering system and cannot be directly promoted to code." + selected: false + - option: "Sandboxed UI component preview" + description: "The agent generates a standalone React component rendered in an iframe or sandbox within the Web UI. Provides the richest visual feedback but requires a component sandbox runtime, significantly increasing complexity." + selected: true + selectionRationale: "Code diff in worktree is the most practical approach. It reuses the entire existing infrastructure (worktree service, agent executor, git operations) and produces something that can be directly promoted. Users can review diffs in the Web UI artifact tab, check out the branch locally to test, or promote the code as-is. The key insight is that exploration prototypes should be real code — just produced with different prompting (speed over quality, minimal scope, throwaway mindset)." + answer: "Sandboxed UI component preview" + - question: "How should the feedback loop work when the user wants to iterate on a prototype?" + resolved: true + options: + - option: "LangGraph interrupt with resume" + description: "After generating a prototype, the graph interrupts and waits for user feedback. The user provides text feedback via the Web UI or CLI, and the graph resumes with that feedback as state. The agent applies the feedback and generates an updated prototype. Uses the existing HITL interrupt/resume mechanism already proven with approval gates." + selected: true + - option: "New agent run per iteration" + description: "Each feedback round spawns a new agent run that receives the previous prototype context plus new feedback. Simpler graph topology (no loops) but loses checkpoint continuity and requires context reconstruction. Each iteration is a fresh start with context passed as input." + selected: false + selectionRationale: "LangGraph interrupt with resume is the natural fit. The codebase already has a robust interrupt/resume mechanism used for approval gates (interrupt() call, --resume-from-interrupt, Command({resume, update})). The feedback loop is conceptually identical: pause, get human input, resume. This approach preserves full agent context across iterations and avoids the overhead of spawning new processes." + answer: "LangGraph interrupt with resume" + - question: "Should there be a limit on the number of feedback iterations per exploration?" + resolved: true + options: + - option: "Configurable limit with sensible default" + description: "Set a default maximum (e.g., 10 iterations) configurable via workflow settings. Prevents runaway loops and unbounded resource usage while allowing power users to increase the limit. The agent notifies the user when approaching the limit." + selected: false + - option: "Unlimited iterations" + description: "No limit on feedback rounds. Exploration continues until the user explicitly promotes or discards. Simpler implementation but risks unbounded resource consumption and endlessly diverging prototypes." + selected: true + selectionRationale: "A configurable limit protects against unbounded resource usage while maintaining flexibility. 10 iterations is generous for exploration — if a user needs more than 10 rounds, the exploration has likely diverged and should be restarted with clearer direction. The limit is a guardrail, not a hard wall." + answer: "Unlimited iterations" + - question: "How should promotion from exploration to regular/fast feature work?" + resolved: true + options: + - option: "In-place mode transition" + description: "Change the feature mode from Exploration to Regular or Fast, transition lifecycle to the appropriate starting state, and spawn the appropriate agent graph. The existing worktree, branch, and any generated code carry over. This is the simplest path and preserves history." + selected: true + - option: "Create new feature from exploration" + description: "Create a brand new Feature record in Regular or Fast mode, optionally cherry-picking code from the exploration worktree. The exploration feature moves to Archived state. Provides a clean separation between exploration and real implementation but loses continuity." + selected: false + selectionRationale: "In-place mode transition is the elegant solution. The Feature entity already has the mode field and lifecycle state — changing them is a state transition, not a data migration. The prototype code in the worktree becomes the starting point for the real implementation. If the user wants a clean start, they can still discard the exploration and create a fresh feature. This approach avoids entity duplication and preserves the iteration history as context for the real implementation." + answer: "In-place mode transition" + - question: "Should exploration mode skip spec initialization entirely or use a minimal spec?" + resolved: true + options: + - option: "Minimal spec (feature.yaml only)" + description: "Initialize with only feature.yaml containing the exploration prompt and metadata. No research/plan/tasks YAMLs since exploration bypasses SDLC phases. On promotion, the full spec set can be generated if transitioning to regular mode." + selected: true + - option: "No spec initialization" + description: "Skip spec directory creation entirely. Exploration features are purely agent-driven with no spec artifacts. Simpler but breaks the convention that all features have a spec directory, complicating queries and UI expectations." + selected: false + - option: "Full spec scaffolding" + description: "Initialize with the full YAML template set same as regular mode. Provides consistency but creates unused files that may confuse users and adds unnecessary overhead." + selected: false + selectionRationale: "Minimal spec matches the exploration philosophy: lightweight, just enough structure to be trackable. feature.yaml captures the exploration prompt and metadata. The spec initializer already supports a mode parameter (fast mode creates minimal specs) so this is a natural extension. On promotion to regular mode, the missing YAMLs can be scaffolded." + answer: "Minimal spec (feature.yaml only)" + - question: "How should exploration features appear on the Web UI canvas?" + resolved: true + options: + - option: "Distinct visual style on existing canvas" + description: "Exploration features appear as nodes on the same canvas but with a distinct visual treatment — different color (e.g., amber/yellow), a beaker/flask icon, and an iteration badge showing the feedback round count. Reuses existing React Flow node infrastructure with style variations." + selected: true + - option: "Separate exploration section/canvas" + description: "Exploration features have their own dedicated area or tab separate from the main feature canvas. Provides clear separation but fragments the user's view and requires significant UI work." + selected: false + selectionRationale: "A distinct visual style on the existing canvas is the right balance. Users should see all their work in one place — explorations and real features coexist. The visual distinction (color, icon, badge) makes it immediately clear which features are explorations without fragmenting the workspace. This approach minimizes UI work by extending the existing node component rather than building a parallel canvas." + answer: "Distinct visual style on existing canvas" +content: "## Problem Statement\n\nCurrently, Shep offers two modes for feature development:\n\n1. **Regular mode** (full SDLC): Analyze -> Requirements -> Research -> Planning -> Implementation -> Review -> Maintain.\n Best for well-understood, complex features but heavy for exploration.\n\n2. **Fast mode**: Skips all SDLC phases, sends the user prompt directly to the executor for\n immediate implementation. Best for small, clear changes but produces committed code immediately.\n\nNeither mode supports the common scenario where a user has a vague idea and needs to explore\nmultiple directions before committing. For example: \"I want to add workspaces or projects to\ngroup repos/features\" — the user doesn't yet know what this means architecturally, what the\nUI should look like, or which approach is best. They need to see quick prototypes, evaluate\nthem visually, provide feedback, and iterate before deciding on a direction.\n\nThe exploration/prototyping mode fills this gap by enabling a discovery-first workflow:\ndescribe an idea -> see a quick prototype -> iterate with feedback -> approve and promote\nto a full feature for proper implementation.\n\n## Success Criteria\n\n- [ ] User can create an exploration-mode feature from the Web UI via a mode selector (Regular / Fast / Explore)\n- [ ] User can create an exploration-mode feature from the CLI via `shep feat new --explore`\n- [ ] Exploration agent generates a working code prototype in an isolated worktree within 2 minutes of creation\n- [ ] User can view the generated prototype diff in the Web UI feature drawer\n- [ ] User can provide text feedback on a prototype and the agent iterates, producing an updated prototype\n- [ ] Feedback loop supports at least 5 consecutive iterations without context degradation\n- [ ] User can promote an exploration feature to Regular or Fast mode, preserving the prototype code\n- [ ] User can discard an exploration feature, cleaning up the worktree and branch\n- [ ] Exploration features are visually distinct on the canvas (different color/icon from regular and fast features)\n- [ ] Exploration features show iteration count (e.g., \"Iteration 3/10\") in the UI\n- [ ] FeatureMode enum replaces the boolean fast field across domain, persistence, and presentation layers\n- [ ] All existing fast-mode functionality continues to work after the boolean-to-enum migration\n- [ ] All new UI components have colocated Storybook stories\n- [ ] Unit tests cover the exploration agent graph, mode transition logic, and feedback loop\n- [ ] Integration tests cover the create-explore-iterate-promote workflow end-to-end\n\n## Functional Requirements\n\n- **FR-1: FeatureMode enum** — Replace the boolean `fast` field on the Feature entity with a `FeatureMode` enum (`Regular`, `Fast`, `Exploration`) defined in TypeSpec. Migrate all code paths that check `fast` boolean to use the enum. This is a prerequisite for all other requirements.\n\n- **FR-2: Create exploration feature** — The `CreateFeatureUseCase` must accept `mode: FeatureMode.Exploration` as input and create a Feature record with lifecycle state `Exploring`. The use case must initialize a minimal spec (feature.yaml only), create an isolated worktree, and spawn the exploration agent graph.\n\n- **FR-3: Exploration agent graph** — A new LangGraph graph factory (`createExplorationAgentGraph`) with the following topology: `START -> prototype-generate -> interrupt(feedback) -> apply-feedback -> prototype-generate -> ... -> END`. The graph generates a quick code prototype, interrupts for user feedback, applies feedback, and loops. The graph exits when the user promotes or discards.\n\n- **FR-4: Prototype generation node** — The `prototype-generate` node invokes the agent executor with prompting optimized for speed and minimal scope: \"generate a quick, working prototype that demonstrates the idea — prioritize showing the concept over production quality.\" The agent writes code changes to the worktree.\n\n- **FR-5: Feedback interrupt** — After each prototype generation, the graph interrupts using the existing LangGraph interrupt mechanism. The interrupt payload includes a summary of what was generated and the iteration count. The user provides text feedback or a promote/discard action.\n\n- **FR-6: Apply feedback node** — The `apply-feedback` node receives user feedback text and prepares context for the next prototype-generate iteration. It accumulates feedback history in the graph state so the agent has full iteration context.\n\n- **FR-7: Iteration limit** — The feedback loop enforces a configurable maximum iteration count (default: 10). When the limit is reached, the graph interrupts with a final prompt asking the user to promote, discard, or reset the exploration. The limit is configurable in workflow settings.\n\n- **FR-8: Promote exploration to feature** — A `PromoteExplorationUseCase` transitions an exploration feature to Regular or Fast mode. For Regular mode: transitions lifecycle to `Requirements` (or `Implementation` if the user wants to skip SDLC) and optionally scaffolds full spec YAMLs. For Fast mode: transitions lifecycle to `Implementation`. The existing worktree code is preserved as the starting point. The appropriate agent graph is spawned.\n\n- **FR-9: Discard exploration** — A `DiscardExplorationUseCase` (or extension of existing delete logic) moves an exploration feature to `Deleting` state, cleans up the worktree and branch, and transitions to `Archived`. Reuses existing feature deletion infrastructure.\n\n- **FR-10: Web UI mode selector** — The feature creation drawer replaces the fast-mode toggle with a three-way mode selector (Regular / Fast / Explore). When Explore is selected, approval gates and SDLC-specific options are hidden since they do not apply. The mode selector defaults to the user's workflow settings preference.\n\n- **FR-11: Web UI prototype review** — The feature drawer for exploration features shows: (a) the current prototype diff in the artifacts/code tab, (b) a feedback input area with a text field and submit button, (c) a promote button that opens a mode selection dialog (Regular or Fast), and (d) a discard button. The iteration count is displayed prominently.\n\n- **FR-12: Web UI canvas treatment** — Exploration features render on the canvas with a distinct visual style: amber/yellow color scheme, a beaker/flask icon, and a badge showing the current iteration count. Clicking opens the feature drawer with the prototype review tab.\n\n- **FR-13: CLI exploration support** — Add `--explore` flag to `shep feat new`. Add `shep feat feedback ` command for providing feedback from CLI. Add `shep feat promote [--fast]` command for promoting. The CLI displays prototype summaries and iteration status.\n\n- **FR-14: Worker process routing** — The feature agent worker selects the exploration graph factory when mode is `Exploration`, alongside the existing fast/regular routing. The worker handles interrupt/resume for the feedback loop using the same mechanism as approval gates.\n\n- **FR-15: Exploring lifecycle state** — Add `Exploring` to the `SdlcLifecycle` enum in TypeSpec. Define lifecycle gates for exploration: the only valid transitions are `Exploring -> Implementation` (promote to fast), `Exploring -> Requirements` (promote to regular), `Exploring -> Deleting` (discard), and self-loop for iterations.\n\n- **FR-16: Exploration state channels** — Extend `FeatureAgentAnnotation` with exploration-specific state channels: `iterationCount` (number), `maxIterations` (number), `feedbackHistory` (string array), `explorationStatus` (generating/waiting-feedback/applying-feedback/promoting/discarding).\n\n- **FR-17: Settings integration** — Add `workflow.defaultMode` setting (replacing or extending `workflow.defaultFastMode`) that accepts `regular`, `fast`, or `exploration`. The Web UI mode selector and CLI default to this preference.\n\n## Non-Functional Requirements\n\n- **NFR-1: Prototype generation speed** — The initial prototype generation must complete within 2 minutes for typical prompts. Subsequent feedback iterations must complete within 90 seconds. The exploration agent prompt must explicitly prioritize speed over completeness.\n\n- **NFR-2: Backward compatibility** — The FeatureMode enum migration must not break existing features. Existing features with `fast=true` must map to `FeatureMode.Fast` and `fast=false` to `FeatureMode.Regular`. Database migration must be reversible.\n\n- **NFR-3: Resource isolation** — Each exploration feature runs in its own worktree and branch, same as regular features. No shared state between exploration features. Exploration worktrees are cleaned up on discard.\n\n- **NFR-4: Context preservation** — The feedback loop must preserve full agent context across iterations via LangGraph checkpointing. The agent must be able to reference all previous feedback and prototype state when generating the next iteration.\n\n- **NFR-5: UI responsiveness** — The Web UI must remain responsive during prototype generation. SSE events provide real-time status updates (generating, ready for feedback, applying feedback). No blocking operations in the main thread.\n\n- **NFR-6: Storybook coverage** — Every new or modified Web UI component must have colocated `.stories.tsx` files with representative states (empty, loading, with data, error, multiple iterations).\n\n- **NFR-7: Test coverage** — Unit tests for the exploration graph nodes, mode transition logic, feedback accumulation, and iteration limits. Integration tests for the full create-iterate-promote workflow. No untested code paths in the new mode.\n\n- **NFR-8: Clean Architecture compliance** — All new code must follow the existing four-layer architecture. The exploration agent graph lives in infrastructure. Use cases live in application. The FeatureMode enum and Exploring lifecycle state live in domain (via TypeSpec). Presentation layers call use cases only.\n\n- **NFR-9: Agent-agnostic execution** — The exploration agent graph must work with any configured agent executor (Claude Code, Gemini CLI, Cursor, Codex) via the existing `IAgentExecutorProvider` interface. No hardcoded provider references.\n\n## Product Questions & AI Recommendations\n\n| # | Question | AI Recommendation | Rationale |\n| - | -------- | ----------------- | --------- |\n| 1 | Mode modeling: enum vs additional boolean? | FeatureMode enum | Prevents invalid state combinations, scales to future modes, cleaner domain model |\n| 2 | Entity model: reuse Feature or new Prototype entity? | Reuse Feature with Exploring lifecycle state | Avoids massive infrastructure duplication, promotion is a state transition not entity migration |\n| 3 | Prototype artifact format? | Code diff in worktree | Reuses existing infrastructure, produces promotable code, users can test locally |\n| 4 | Feedback loop mechanism? | LangGraph interrupt with resume | Leverages existing HITL mechanism, preserves agent context, proven pattern |\n| 5 | Iteration limit? | Configurable limit, default 10 | Prevents runaway loops, protects resources, power users can increase |\n| 6 | Promotion mechanism? | In-place mode transition | Simplest path, preserves history, no entity duplication |\n| 7 | Spec initialization for exploration? | Minimal spec (feature.yaml only) | Matches lightweight philosophy, extensible on promotion |\n| 8 | Canvas treatment? | Distinct visual style on existing canvas | Unified workspace view, minimal UI work, clear visual distinction |\n\n## Affected Areas\n\n| Area | Impact | Reasoning |\n| ---- | ------ | --------- |\n| TypeSpec domain models (`tsp/`) | High | New `FeatureMode` enum replacing the boolean `fast` field; new `Exploring` lifecycle state |\n| CreateFeatureUseCase (`application/`) | High | Must support exploration mode input, different lifecycle initialization, and exploration agent spawning |\n| LangGraph agent graph (`infrastructure/agents/feature-agent/`) | High | New exploration-mode graph factory with prototype-generation nodes, feedback loops, and promote-to-feature edges |\n| Feature Agent Worker (`infrastructure/agents/feature-agent/worker`) | High | Must instantiate the exploration graph when mode is set, handle iteration/feedback cycles |\n| Web UI Create Drawer (`presentation/web/feature-create-drawer/`) | High | New mode selector (Regular/Fast/Explore), exploration-specific UI for viewing prototypes, providing feedback, and promoting |\n| Web UI Feature Drawer Tabs (`presentation/web/feature-drawer-tabs/`) | High | New prototype preview tab or panel for viewing exploration results, iteration history |\n| Web UI Control Center Canvas (`presentation/web/control-center/`) | Medium | Exploration features need distinct visual treatment on the canvas (different node style/color/icon) |\n| CLI feat new command (`presentation/cli/commands/feat/`) | Medium | New `--explore` flag, new `feedback` and `promote` subcommands |\n| Spec Initializer (`infrastructure/services/spec/`) | Medium | Exploration mode needs different minimal spec scaffolding |\n| PromoteExplorationUseCase (`application/`) | Medium | New use case for mode transition and graph re-spawning |\n| Settings / WorkflowConfig (`tsp/domain/entities/settings.tsp`) | Low | `workflow.defaultMode` setting replacing `workflow.defaultFastMode` |\n| Feature Repository (`infrastructure/repositories/`) | Low | Column migration from boolean fast to enum mode; query support for filtering by mode |\n| Domain lifecycle gates (`domain/lifecycle-gates.ts`) | Low | New gate sets for the Exploring lifecycle state and valid transitions |\n\n## Dependencies\n\n- **Existing fast-mode infrastructure**: The exploration mode follows the same architectural\n pattern as fast mode (separate graph factory, worker flag routing, UI toggle). Understanding\n and reusing this pattern is critical.\n- **LangGraph checkpoint/interrupt system**: The iterative feedback loop in exploration mode\n requires interrupt points where the user can review a prototype and provide feedback. This\n uses the same HITL interrupt mechanism as approval gates.\n- **Worktree service**: Exploration prototypes need isolated git worktrees, same as regular\n features. The existing `IWorktreeService` handles this.\n- **Feature entity and lifecycle system**: Exploration reuses the Feature entity with a new\n Exploring lifecycle state. The FeatureMode enum replaces the boolean fast field.\n- **Web UI drawer/tabs system**: The existing `BaseDrawer`, `FeatureCreateDrawer`, and\n feature drawer tabs provide the framework for the exploration UI. New tabs or views for\n prototype preview and iteration extend this system.\n- **Agent executor interface**: The exploration prototype agent uses the same\n `IAgentExecutor` interface, just with different prompts optimized for quick prototyping.\n- **FeatureMode enum migration**: The boolean-to-enum migration (FR-1) is a prerequisite\n for all other exploration mode work. It must be completed and verified first.\n\n## Size Estimate\n\n**XL** — This feature introduces a fundamentally new workflow mode that touches every layer\nof the architecture: new TypeSpec domain models (FeatureMode enum, Exploring lifecycle state),\na new LangGraph graph with iterative feedback nodes, significant Web UI work for prototype\nreview and iteration, CLI support with new subcommands, and a FeatureMode enum migration.\nThe scope is comparable to the original feature-agent implementation. The iterative feedback\nloop and the boolean-to-enum migration add complexity beyond what fast mode required.\nEstimated at 17 functional requirements across domain, application, infrastructure, and\npresentation layers.\n\n---\n\n_Requirements complete — proceed with research to resolve technical approach_\n" +rejectionFeedback: + - iteration: 1 + message: "Resolve merge conflicts" + phase: "merge" + timestamp: "2026-04-02T21:56:51.411Z" + - iteration: 2 + message: "clicking on Explore should make it selected" + phase: "merge" + timestamp: "2026-04-03T12:50:26.428Z" + - iteration: 3 + message: "merge remote main and resolve conflcits" + phase: "merge" + timestamp: "2026-04-05T16:06:24.308Z" diff --git a/specs/082-prototype-exploration-mode/tasks.yaml b/specs/082-prototype-exploration-mode/tasks.yaml new file mode 100644 index 000000000..3e4cc0655 --- /dev/null +++ b/specs/082-prototype-exploration-mode/tasks.yaml @@ -0,0 +1,864 @@ +name: "prototype-exploration-mode" +summary: > + 29 tasks across 7 phases implementing exploration/prototyping mode. Starts with TypeSpec + domain models (FeatureMode enum, Exploring lifecycle state, iteration fields), then persistence + migration (fast boolean to mode enum), agent graph infrastructure (exploration graph factory, + prototype-generate and apply-feedback nodes, worker routing), application use cases (create, + promote, discard), CLI commands (--explore, feedback, promote), Web UI (mode selector, canvas + nodes, prototype drawer tab), and settings integration (defaultMode, maxIterations). + +relatedFeatures: + - "052-fast-prompt-to-pr" + +technologies: + - "TypeScript" + - "TypeSpec" + - "LangGraph (@langchain/langgraph)" + - "React / Next.js" + - "Commander.js" + - "shadcn/ui" + - "SQLite (better-sqlite3)" + - "tsyringe (DI)" + - "React Flow (@xyflow/react)" + - "Lucide React (icons)" + - "Storybook" + - "Vitest" + +relatedLinks: + - title: "LangGraph Interrupt/Resume Documentation" + url: "https://langchain-ai.github.io/langgraphjs/how-tos/human_in_the_loop/breakpoints/" + - title: "TypeSpec Enum Documentation" + url: "https://typespec.io/docs/language-basics/enums" + +tasks: + # ============================================================ + # Phase 1: Domain Foundation (TypeSpec + Generated Code) + # ============================================================ + + - id: "task-1" + phaseId: "phase-1" + title: "Define FeatureMode enum in TypeSpec" + description: "Create a new FeatureMode enum in tsp/common/enums/feature-mode.tsp with values Regular, Fast, and Exploration. Follow the pattern of existing enums like SdlcLifecycle. Import and use the new enum in tsp/domain/entities/feature.tsp to replace the boolean fast field." + state: "Todo" + dependencies: [] + acceptanceCriteria: + - "FeatureMode enum exists in tsp/common/enums/feature-mode.tsp with Regular, Fast, Exploration values" + - "Feature entity in feature.tsp uses FeatureMode instead of boolean fast" + - "pnpm tsp:compile succeeds and generates output.ts with FeatureMode" + - "Generated output.ts includes FeatureMode type and enum values" + tdd: + red: + - "Write test asserting FeatureMode enum has exactly three values: Regular, Fast, Exploration" + - "Write test asserting Feature type has mode field of type FeatureMode instead of fast boolean" + green: + - "Create tsp/common/enums/feature-mode.tsp with FeatureMode enum" + - "Update tsp/domain/entities/feature.tsp to replace fast: boolean with mode: FeatureMode" + - "Run pnpm tsp:compile to regenerate output.ts" + refactor: + - "Ensure enum doc comments match the domain language from the spec" + estimatedEffort: "45min" + + - id: "task-2" + phaseId: "phase-1" + title: "Add Exploring state to SdlcLifecycle enum" + description: "Add Exploring to the SdlcLifecycle enum in tsp/common/enums/lifecycle.tsp. Position it logically near Started since it represents an early-stage exploratory state. Recompile TypeSpec." + state: "Todo" + dependencies: + - "task-1" + acceptanceCriteria: + - "SdlcLifecycle enum includes Exploring state" + - "pnpm tsp:compile succeeds" + - "Generated output.ts includes Exploring in the lifecycle type" + tdd: + red: + - "Write test asserting SdlcLifecycle includes 'Exploring' as a valid value" + green: + - "Add Exploring variant to SdlcLifecycle in lifecycle.tsp" + - "Run pnpm tsp:compile" + refactor: + - "Add doc comment explaining Exploring state purpose and valid transitions" + estimatedEffort: "20min" + + - id: "task-3" + phaseId: "phase-1" + title: "Add iteration fields to Feature entity in TypeSpec" + description: "Add iterationCount (integer, default 0) and maxIterations (optional integer) fields to the Feature model in feature.tsp. These fields track exploration feedback loop progress and are persisted to the database for Web UI access." + state: "Todo" + dependencies: + - "task-1" + acceptanceCriteria: + - "Feature entity has iterationCount: int32 = 0 field" + - "Feature entity has maxIterations?: int32 optional field" + - "pnpm tsp:compile succeeds" + - "Generated output.ts includes both fields on Feature type" + tdd: + red: + - "Write test asserting Feature type has iterationCount (number) and maxIterations (optional number) fields" + green: + - "Add iterationCount and maxIterations fields to Feature model in feature.tsp" + - "Run pnpm tsp:compile" + refactor: + - "Add doc comments explaining field purpose and when they are populated" + estimatedEffort: "15min" + + - id: "task-4" + phaseId: "phase-1" + title: "Update lifecycle gate definitions for Exploring state" + description: "Add lifecycle gate configuration for the Exploring state in the domain lifecycle gates module. Define valid transitions: Exploring -> Implementation (promote-fast), Exploring -> Requirements (promote-regular), Exploring -> Deleting (discard). The Exploring state has no approval gates since exploration bypasses SDLC." + state: "Todo" + dependencies: + - "task-2" + acceptanceCriteria: + - "Lifecycle gates module includes Exploring state with valid transition targets" + - "Exploring state transitions to Implementation, Requirements, and Deleting only" + - "No approval gates defined for Exploring state" + - "Existing lifecycle gate tests still pass" + tdd: + red: + - "Write test asserting Exploring state has valid transitions to Implementation, Requirements, and Deleting" + - "Write test asserting Exploring state has no approval gates" + - "Write test asserting invalid transition from Exploring to Review throws error" + green: + - "Add Exploring configuration to lifecycle gates" + - "Define valid transition targets" + refactor: + - "Ensure gate definitions follow the same pattern as existing states" + estimatedEffort: "30min" + + # ============================================================ + # Phase 2: Persistence Migration (Boolean-to-Enum + Schema) + # ============================================================ + + - id: "task-5" + phaseId: "phase-2" + title: "Create SQLite migration to replace fast boolean with mode enum" + description: "Create migration 051-replace-fast-with-mode.ts that adds a TEXT mode column to the features table, migrates existing data (fast=1 -> 'Fast', fast=0 -> 'Regular'), and drops the fast column via SQLite table recreation. Register the migration in the migrations index." + state: "Todo" + dependencies: + - "task-1" + acceptanceCriteria: + - "Migration file exists at 051-replace-fast-with-mode.ts" + - "Migration adds TEXT mode column with DEFAULT 'Regular'" + - "Migration maps fast=1 to mode='Fast' and fast=0 to mode='Regular'" + - "Migration drops the fast column after data migration" + - "Migration is registered in migrations/index.ts" + - "Migration handles empty tables gracefully" + tdd: + red: + - "Write integration test creating a test DB with features (fast=0 and fast=1), running migration, asserting mode column values are 'Regular' and 'Fast'" + - "Write test asserting fast column no longer exists after migration" + green: + - "Implement migration using table recreation pattern (create temp, copy data, drop old, rename)" + - "Add migration to index.ts exports" + refactor: + - "Add comments explaining the SQLite table recreation pattern" + estimatedEffort: "1h" + + - id: "task-6" + phaseId: "phase-2" + title: "Update feature mapper from boolean fast to enum mode" + description: "Update feature.mapper.ts to map between FeatureMode enum (domain) and TEXT mode column (database). Replace toDatabase: fast ? 1 : 0 with mode string, and toDomain: row.fast === 1 with FeatureMode enum parsing. Add iterationCount and maxIterations field mapping." + state: "Todo" + dependencies: + - "task-3" + - "task-5" + acceptanceCriteria: + - "toDatabase maps FeatureMode enum to string ('Regular', 'Fast', 'Exploration')" + - "toDomain maps string mode column to FeatureMode enum" + - "toDatabase and toDomain handle iterationCount and maxIterations" + - "No references to boolean fast remain in mapper" + - "Existing mapper tests updated and passing" + tdd: + red: + - "Write test: toDatabase with mode=FeatureMode.Fast produces row with mode='Fast'" + - "Write test: toDomain with row.mode='Exploration' produces feature with mode=FeatureMode.Exploration" + - "Write test: toDomain with row.mode='Regular' produces feature with mode=FeatureMode.Regular" + - "Write test: iterationCount and maxIterations round-trip correctly through mapper" + green: + - "Replace fast boolean mapping with mode enum mapping in toDatabase" + - "Replace fast boolean mapping with mode enum parsing in toDomain" + - "Add iterationCount/maxIterations field mapping" + refactor: + - "Remove any dead code related to boolean fast mapping" + - "Update existing tests that reference the fast boolean" + estimatedEffort: "45min" + + - id: "task-7" + phaseId: "phase-2" + title: "Update feature repository queries for mode enum" + description: "Update any repository queries that filter or sort by the fast boolean to use the mode TEXT column instead. Update any query builders or raw SQL that references the fast column." + state: "Todo" + dependencies: + - "task-6" + acceptanceCriteria: + - "No repository queries reference the fast column" + - "Queries that filtered on fast now filter on mode" + - "Repository can query features by mode (findByMode or equivalent)" + - "All existing repository tests pass with mode instead of fast" + tdd: + red: + - "Write test: repository query filters features by mode='Exploration'" + - "Write test: repository returns correct mode value for persisted features" + green: + - "Update SQL queries and query builders from fast column to mode column" + - "Add any new mode-based query methods if needed" + refactor: + - "Search for remaining references to 'fast' in repository layer and update" + estimatedEffort: "30min" + + # ============================================================ + # Phase 3: Agent Graph Infrastructure + # ============================================================ + + - id: "task-8" + phaseId: "phase-3" + title: "Extend FeatureAgentAnnotation with exploration state channels" + description: "Add exploration-specific channels to the FeatureAgentAnnotation in state.ts: iterationCount (number, default 0), maxIterations (number, default 10), feedbackHistory (string array with accumulating reducer), and explorationStatus (string union type). Follow the pattern of existing CI fix channels." + state: "Todo" + dependencies: + - "task-3" + acceptanceCriteria: + - "FeatureAgentAnnotation includes iterationCount channel with default 0" + - "FeatureAgentAnnotation includes maxIterations channel with default 10" + - "FeatureAgentAnnotation includes feedbackHistory channel with accumulating reducer" + - "FeatureAgentAnnotation includes explorationStatus channel" + - "Existing graph tests still pass (new channels have safe defaults)" + tdd: + red: + - "Write test: annotation creates state with iterationCount=0 by default" + - "Write test: feedbackHistory accumulates via reducer ([...prev, ...next])" + - "Write test: explorationStatus defaults to undefined/null for non-exploration graphs" + green: + - "Add four new channels to FeatureAgentAnnotation definition" + - "Implement accumulating reducer for feedbackHistory" + refactor: + - "Group exploration channels together with a comment block, similar to CI fix channels" + estimatedEffort: "30min" + + - id: "task-9" + phaseId: "phase-3" + title: "Create exploration prompt builders" + description: "Create buildPrototypeGeneratePrompt in prototype-generate.prompt.ts and buildApplyFeedbackPrompt in apply-feedback.prompt.ts. The generate prompt emphasizes quick prototyping, minimal scope, and throwaway quality. The feedback prompt includes previous prototype context, accumulated feedback history, and current iteration number." + state: "Todo" + dependencies: + - "task-8" + acceptanceCriteria: + - "buildPrototypeGeneratePrompt produces a prompt emphasizing speed over quality" + - "buildPrototypeGeneratePrompt includes user query, repo context, and iteration context" + - "buildApplyFeedbackPrompt includes feedback text, iteration history summary, and instruction to modify prototype" + - "Both prompt builders handle first iteration (no prior feedback) and subsequent iterations (with history)" + - "Older feedback (beyond 3 iterations) is summarized to manage context window" + tdd: + red: + - "Write test: buildPrototypeGeneratePrompt includes user query in output" + - "Write test: buildPrototypeGeneratePrompt on iteration 0 has no feedback context" + - "Write test: buildPrototypeGeneratePrompt on iteration 3 includes summarized history" + - "Write test: buildApplyFeedbackPrompt includes current feedback text" + - "Write test: buildApplyFeedbackPrompt includes iteration count" + green: + - "Implement buildPrototypeGeneratePrompt with conditional history inclusion" + - "Implement buildApplyFeedbackPrompt with feedback context building" + refactor: + - "Extract shared prompt utilities if patterns emerge between generate and feedback prompts" + estimatedEffort: "1h" + + - id: "task-10" + phaseId: "phase-3" + title: "Implement prototype-generate node" + description: "Create the prototype-generate node function in prototype-generate.node.ts. This node calls the agent executor with the exploration prompt, writes code changes to the worktree, updates iterationCount on the feature entity, and interrupts for user feedback. Uses retryExecute and createNodeLogger from shared utilities." + state: "Todo" + dependencies: + - "task-8" + - "task-9" + acceptanceCriteria: + - "Node calls agent executor with prototype generate prompt via retryExecute" + - "Node updates iterationCount in graph state" + - "Node updates feature entity iterationCount in database" + - "Node interrupts after generation with summary payload" + - "Node respects maxIterations limit" + - "Node uses createNodeLogger for consistent logging" + tdd: + red: + - "Write test: node calls executor with prototype generate prompt" + - "Write test: node increments iterationCount in returned state" + - "Write test: node interrupts after generation" + - "Write test: node includes iteration count in interrupt payload" + - "Write test: node forces promote/discard when iterationCount reaches maxIterations" + green: + - "Implement createPrototypeGenerateNode factory function" + - "Call retryExecute with exploration prompt" + - "Return state update with incremented iterationCount and messages" + - "Call interrupt() with generation summary" + refactor: + - "Ensure error handling follows the pattern of other nodes (re-throw with node name prefix)" + estimatedEffort: "1.5h" + + - id: "task-11" + phaseId: "phase-3" + title: "Implement apply-feedback node" + description: "Create the apply-feedback node function in apply-feedback.node.ts. This node receives user feedback from the resume payload, appends it to feedbackHistory, updates explorationStatus, and prepares context for the next prototype-generate iteration. It does NOT call the agent executor — it is a state-transformation node." + state: "Todo" + dependencies: + - "task-8" + acceptanceCriteria: + - "Node extracts feedback text from resume payload" + - "Node appends feedback to feedbackHistory accumulator" + - "Node updates explorationStatus to 'applying-feedback'" + - "Node returns state ready for next prototype-generate iteration" + - "Node handles empty or missing feedback gracefully" + tdd: + red: + - "Write test: node appends feedback text to feedbackHistory" + - "Write test: node updates explorationStatus to 'applying-feedback'" + - "Write test: node handles empty feedback string gracefully" + - "Write test: node preserves existing feedbackHistory entries" + green: + - "Implement createApplyFeedbackNode factory function" + - "Extract feedback from state (populated by Command resume)" + - "Return state update with appended feedbackHistory" + refactor: + - "Ensure the node is pure state transformation with no side effects" + estimatedEffort: "45min" + + - id: "task-12" + phaseId: "phase-3" + title: "Create exploration agent graph factory" + description: "Create createExplorationAgentGraph in exploration-agent-graph.ts with topology: START -> prototype-generate -> interrupt -> apply-feedback -> prototype-generate (loop). The graph exits via conditional routing when the resume action is 'promote' or 'discard' instead of 'iterate'. Follow the factory pattern of createFastFeatureAgentGraph." + state: "Todo" + dependencies: + - "task-10" + - "task-11" + acceptanceCriteria: + - "Graph factory creates a compiled StateGraph with FeatureAgentAnnotation" + - "Graph topology: START -> prototype-generate, prototype-generate -> apply-feedback (on iterate), prototype-generate -> END (on promote/discard)" + - "Graph accepts checkpointer parameter for interrupt/resume" + - "Graph deps interface follows the FeatureAgentGraphDeps pattern" + - "Graph handles the feedback resume payload to route correctly" + tdd: + red: + - "Write test: graph factory returns compiled graph" + - "Write test: graph invocation calls prototype-generate node first" + - "Write test: graph interrupts after prototype-generate" + - "Write test: graph routes to apply-feedback on 'iterate' resume action" + - "Write test: graph routes to END on 'promote' resume action" + - "Write test: graph routes to END on 'discard' resume action" + green: + - "Implement createExplorationAgentGraph with StateGraph construction" + - "Add nodes: prototype-generate, apply-feedback" + - "Add edges: START -> prototype-generate" + - "Add conditional routing from resume payload" + - "Compile graph with checkpointer" + refactor: + - "Align factory signature and deps interface with existing graph factories" + - "Extract routing function for testability" + estimatedEffort: "1.5h" + + - id: "task-13" + phaseId: "phase-3" + title: "Update worker routing from --fast boolean to --mode enum" + description: "Replace the --fast boolean flag in feature-agent-worker.ts with --mode string flag accepting 'regular', 'fast', or 'exploration'. Update WorkerArgs interface, parseWorkerArgs function, and graph selection logic to route based on mode value. Update feature-agent-process.service.ts to pass --mode instead of --fast." + state: "Todo" + dependencies: + - "task-12" + acceptanceCriteria: + - "WorkerArgs has mode: string instead of fast: boolean" + - "parseWorkerArgs parses --mode flag correctly" + - "Graph selection uses mode value: 'exploration' -> exploration graph, 'fast' -> fast graph, 'regular' -> regular graph" + - "Process service passes --mode= instead of --fast" + - "Worker handles missing --mode flag with default 'regular' for backward compatibility" + tdd: + red: + - "Write test: parseWorkerArgs parses --mode=exploration correctly" + - "Write test: parseWorkerArgs parses --mode=fast correctly" + - "Write test: parseWorkerArgs defaults to 'regular' when --mode is absent" + - "Write test: graph selection returns exploration graph for mode='exploration'" + - "Write test: graph selection returns fast graph for mode='fast'" + - "Write test: process service spawns worker with --mode=exploration for exploration features" + green: + - "Replace --fast with --mode in WorkerArgs and parseWorkerArgs" + - "Update graph selection from boolean check to switch on mode" + - "Update process service to pass --mode flag" + refactor: + - "Remove dead code related to --fast flag" + - "Ensure backward compatibility for any workers started with old --fast flag" + estimatedEffort: "1h" + + # ============================================================ + # Phase 4: Application Use Cases + # ============================================================ + + - id: "task-14" + phaseId: "phase-4" + title: "Extend CreateFeatureUseCase for exploration mode" + description: "Update CreateFeatureUseCase to accept mode: FeatureMode as input instead of fast: boolean. When mode is Exploration, set initial lifecycle to Exploring, use minimal spec initialization (feature.yaml only), and spawn the exploration agent graph. Update all callers to pass mode enum." + state: "Todo" + dependencies: + - "task-4" + - "task-7" + - "task-13" + acceptanceCriteria: + - "CreateFeatureInput accepts mode: FeatureMode instead of fast: boolean" + - "mode=Exploration sets lifecycle to SdlcLifecycle.Exploring" + - "mode=Exploration triggers exploration spec initialization (feature.yaml only)" + - "mode=Exploration spawns worker with --mode=exploration" + - "mode=Fast preserves existing fast mode behavior" + - "mode=Regular preserves existing regular mode behavior" + - "All existing create feature tests updated for mode enum" + tdd: + red: + - "Write test: creating feature with mode=Exploration sets lifecycle to Exploring" + - "Write test: creating feature with mode=Exploration initializes minimal spec" + - "Write test: creating feature with mode=Exploration spawns worker with exploration mode" + - "Write test: creating feature with mode=Fast still works (backward compatibility)" + - "Write test: creating feature with mode=Regular still works (backward compatibility)" + green: + - "Replace fast: boolean with mode: FeatureMode in CreateFeatureInput" + - "Update lifecycle selection logic for three modes" + - "Update spec initializer call to pass 'exploration' mode" + - "Update process service spawn call to pass mode" + refactor: + - "Update all callers of CreateFeatureUseCase (CLI, Web UI server action)" + estimatedEffort: "1.5h" + + - id: "task-15" + phaseId: "phase-4" + title: "Update spec initializer for exploration mode" + description: "Extend the spec initializer service to accept mode='exploration' and generate only feature.yaml (no spec.yaml, research.yaml, plan.yaml, or tasks.yaml). Follow the existing pattern where fast mode already generates a minimal subset." + state: "Todo" + dependencies: + - "task-14" + acceptanceCriteria: + - "Spec initializer accepts mode='exploration' parameter" + - "mode='exploration' generates only feature.yaml" + - "mode='fast' still generates feature.yaml + spec.yaml (unchanged)" + - "mode=undefined still generates all 5 files (unchanged)" + - "feature.yaml for exploration includes the exploration prompt as description" + tdd: + red: + - "Write test: initializer with mode='exploration' creates only feature.yaml" + - "Write test: initializer with mode='exploration' does not create spec.yaml" + - "Write test: initializer with mode='fast' still creates feature.yaml and spec.yaml" + green: + - "Add 'exploration' case to mode-based template filtering" + - "Only include feature.yaml template for exploration mode" + refactor: + - "Consider extracting mode-based template selection into a named function for clarity" + estimatedEffort: "30min" + + - id: "task-16" + phaseId: "phase-4" + title: "Create PromoteExplorationUseCase" + description: "Create a new use case that transitions an exploration feature to Regular or Fast mode. Changes mode field, transitions lifecycle (Exploring -> Requirements for regular, Exploring -> Implementation for fast), optionally scaffolds missing spec YAMLs when promoting to regular, and spawns the appropriate agent graph." + state: "Todo" + dependencies: + - "task-14" + acceptanceCriteria: + - "Use case validates feature is in Exploration mode and Exploring lifecycle" + - "Promote to Regular: mode=Regular, lifecycle=Requirements, scaffolds missing spec YAMLs" + - "Promote to Fast: mode=Fast, lifecycle=Implementation, no spec scaffolding" + - "Preserves existing worktree and branch (no cleanup)" + - "Spawns appropriate agent graph after transition" + - "Returns updated feature entity" + - "Rejects promotion of non-exploration features with descriptive error" + tdd: + red: + - "Write test: promoting exploration feature to Regular changes mode and lifecycle" + - "Write test: promoting exploration feature to Fast changes mode and lifecycle" + - "Write test: promoting to Regular scaffolds missing spec YAMLs" + - "Write test: promoting to Fast does not scaffold spec YAMLs" + - "Write test: promoting non-exploration feature throws error" + - "Write test: promoting feature not in Exploring lifecycle throws error" + green: + - "Create PromoteExplorationUseCase with DI for feature repository, spec initializer, process service" + - "Implement execute method with mode transition logic" + - "Call spec initializer for regular promotion" + - "Spawn worker via process service with new mode" + refactor: + - "Follow existing use case patterns (output port interface, result types)" + estimatedEffort: "1.5h" + + - id: "task-17" + phaseId: "phase-4" + title: "Extend discard/delete logic for exploration features" + description: "Ensure the existing feature deletion use case handles exploration features correctly. Exploration features in Exploring lifecycle transition to Deleting, clean up the worktree and branch, and archive. Verify checkpoint cleanup occurs for exploration graphs." + state: "Todo" + dependencies: + - "task-14" + acceptanceCriteria: + - "Deleting an exploration feature transitions Exploring -> Deleting -> Archived" + - "Worktree is cleaned up during deletion" + - "Branch is cleaned up during deletion" + - "Checkpoint database files are cleaned up for discarded explorations" + - "Existing delete functionality for regular/fast features is unchanged" + tdd: + red: + - "Write test: deleting exploration feature in Exploring state succeeds" + - "Write test: deleting exploration feature cleans up worktree" + - "Write test: deleting regular feature still works (no regression)" + green: + - "Add Exploring to the list of valid source states for deletion transition" + - "Ensure worktree cleanup runs for exploration features" + - "Add checkpoint cleanup for exploration graph thread IDs" + refactor: + - "Verify no special-casing is needed beyond allowing Exploring as a valid delete source" + estimatedEffort: "45min" + + # ============================================================ + # Phase 5: CLI Presentation + # ============================================================ + + - id: "task-18" + phaseId: "phase-5" + title: "Add --explore flag to feat new command" + description: "Add --explore flag to the feat new CLI command. When --explore is specified, create the feature with mode=Exploration. Update the mutually exclusive flag logic so --explore, --fast, and default (regular) are exclusive. Update getWorkflowDefaults to return mode enum instead of fast boolean." + state: "Todo" + dependencies: + - "task-14" + acceptanceCriteria: + - "shep feat new --explore creates feature with mode=Exploration" + - "--explore and --fast are mutually exclusive (error if both specified)" + - "Default mode comes from workflow settings (defaultMode)" + - "getWorkflowDefaults returns mode string instead of fast boolean" + - "Existing --fast and --no-fast flags still work" + tdd: + red: + - "Write test: --explore flag sets mode to Exploration in use case input" + - "Write test: --fast and --explore together produces error" + - "Write test: --fast flag sets mode to Fast (backward compat)" + - "Write test: no mode flag uses workflow default" + green: + - "Add --explore option to commander definition" + - "Update flag resolution logic for three-way mode selection" + - "Update getWorkflowDefaults to use mode enum" + - "Pass mode to CreateFeatureUseCase" + refactor: + - "Extract mode resolution into a helper function for clarity" + estimatedEffort: "45min" + + - id: "task-19" + phaseId: "phase-5" + title: "Create feat feedback CLI command" + description: "Create a new feedback.command.ts that implements 'shep feat feedback '. The command sends feedback to an exploration feature by resuming the interrupted agent graph with an iterate action and the provided feedback text." + state: "Todo" + dependencies: + - "task-14" + acceptanceCriteria: + - "shep feat feedback resumes the exploration graph with iterate action" + - "Command validates feature exists and is in Exploring lifecycle" + - "Command shows current iteration count after feedback submission" + - "Command shows error if feature is not in exploration mode" + - "Command is registered in the feat command group" + tdd: + red: + - "Write test: feedback command calls resume with correct action and feedback text" + - "Write test: feedback command rejects non-exploration features" + - "Write test: feedback command displays iteration count" + green: + - "Create feedback.command.ts with commander action" + - "Look up feature, validate mode and lifecycle" + - "Resume agent graph via process service with iterate payload" + - "Display iteration status" + refactor: + - "Follow the pattern of existing feat subcommands (feat start, feat del)" + estimatedEffort: "1h" + + - id: "task-20" + phaseId: "phase-5" + title: "Create feat promote CLI command" + description: "Create a new promote.command.ts that implements 'shep feat promote [--fast]'. The command promotes an exploration feature to Regular mode (default) or Fast mode (with --fast flag) using PromoteExplorationUseCase." + state: "Todo" + dependencies: + - "task-16" + acceptanceCriteria: + - "shep feat promote promotes to Regular mode by default" + - "shep feat promote --fast promotes to Fast mode" + - "Command validates feature is in exploration mode before promoting" + - "Command shows updated feature status after promotion" + - "Command is registered in the feat command group" + tdd: + red: + - "Write test: promote command calls PromoteExplorationUseCase with Regular mode" + - "Write test: promote command with --fast calls PromoteExplorationUseCase with Fast mode" + - "Write test: promote command rejects non-exploration features" + green: + - "Create promote.command.ts with commander action" + - "Parse --fast flag to determine target mode" + - "Call PromoteExplorationUseCase" + - "Display promotion result" + refactor: + - "Register command in feat/index.ts alongside other subcommands" + estimatedEffort: "45min" + + - id: "task-21" + phaseId: "phase-5" + title: "Register new CLI commands in feat command group" + description: "Register feedback and promote commands in the feat command group index. Ensure commands appear in help output and tab completion." + state: "Todo" + dependencies: + - "task-19" + - "task-20" + acceptanceCriteria: + - "feat feedback and feat promote appear in shep feat --help" + - "Commands are properly imported and registered" + - "No circular dependency issues" + tdd: null + estimatedEffort: "15min" + + # ============================================================ + # Phase 6: Web UI Presentation + # ============================================================ + + - id: "task-22" + phaseId: "phase-6" + title: "Create mode selector component (ToggleGroup)" + description: "Create a ModeSelector component using shadcn/ui ToggleGroup with type='single'. Three options: Regular (ClipboardList icon), Fast (Zap icon), Explore (FlaskConical icon). Component receives value and onChange props. Create colocated Storybook stories." + state: "Todo" + dependencies: + - "task-1" + acceptanceCriteria: + - "ModeSelector renders three options with correct icons and labels" + - "Only one option can be selected at a time" + - "Component emits FeatureMode enum value on change" + - "Component accepts controlled value prop" + - "Storybook stories cover all three selected states plus disabled state" + tdd: + red: + - "Write test: ModeSelector renders three options" + - "Write test: clicking an option calls onChange with correct FeatureMode value" + - "Write test: selected state is visually distinguished" + green: + - "Create mode-selector.tsx with ToggleGroup from shadcn/ui" + - "Add icons from lucide-react (ClipboardList, Zap, FlaskConical)" + - "Wire value/onChange for controlled usage" + refactor: + - "Create mode-selector.stories.tsx with all states" + - "Ensure accessible labels and keyboard navigation" + estimatedEffort: "1h" + + - id: "task-23" + phaseId: "phase-6" + title: "Replace fast toggle with mode selector in feature create drawer" + description: "Replace the Switch toggle for fast mode in feature-create-drawer.tsx with the new ModeSelector component. When Explore is selected, hide approval gate options (they do not apply to exploration). Update FeatureCreatePayload from fast: boolean to mode: FeatureMode." + state: "Todo" + dependencies: + - "task-22" + acceptanceCriteria: + - "Feature create drawer shows ModeSelector instead of Switch" + - "FeatureCreatePayload has mode: FeatureMode instead of fast: boolean" + - "Selecting Explore hides approval gate toggles" + - "Selecting Regular shows all options (unchanged behavior)" + - "Selecting Fast hides approval gates (same as current fast toggle)" + - "Default mode matches workflow settings preference" + tdd: + red: + - "Write test: drawer renders ModeSelector instead of Switch" + - "Write test: selecting Explore hides approval gates" + - "Write test: submitting with Explore mode sends mode=Exploration" + - "Write test: selecting Regular shows approval gates" + green: + - "Replace Switch with ModeSelector in drawer JSX" + - "Update form state from fast boolean to mode enum" + - "Conditionally hide approval gates based on mode" + - "Update submit handler to send mode in payload" + refactor: + - "Update any tests that reference the old fast toggle" + estimatedEffort: "1h" + + - id: "task-24" + phaseId: "phase-6" + title: "Add exploring state to canvas node configuration" + description: "Add an 'exploring' entry to feature-node-state-config.ts with amber/yellow color scheme, FlaskConical icon, and iteration badge. Update feature-node.tsx to render the exploration badge with iteration count." + state: "Todo" + dependencies: + - "task-2" + acceptanceCriteria: + - "feature-node-state-config.ts has exploring state with amber colors" + - "Exploring state uses letter 'E' and amber background" + - "feature-node.tsx renders FlaskConical icon for exploration features" + - "Iteration count badge displays current iteration number" + - "Exploring nodes are visually distinct from fast and regular nodes" + tdd: + red: + - "Write test: state config returns correct colors for 'exploring' state" + - "Write test: feature node renders FlaskConical icon when lifecycle is exploring" + - "Write test: feature node shows iteration badge with correct count" + green: + - "Add exploring entry to phase badge config with amber colors" + - "Add FlaskConical icon rendering for exploration mode in feature-node.tsx" + - "Add iteration count badge for exploration features" + refactor: + - "Ensure amber colors work in both light and dark mode" + estimatedEffort: "45min" + + - id: "task-25" + phaseId: "phase-6" + title: "Create prototype review drawer tab" + description: "Create a prototype-tab.tsx component shown when lifecycle is Exploring. The tab displays: (a) prototype diff/code changes, (b) feedback text input with submit button, (c) promote button opening mode selection dialog (Regular/Fast), (d) discard button with confirmation. Create colocated Storybook stories." + state: "Todo" + dependencies: + - "task-14" + - "task-16" + acceptanceCriteria: + - "Prototype tab shows current prototype changes/diff" + - "Feedback input allows entering text and submitting" + - "Submit sends feedback via server action that resumes graph with iterate action" + - "Promote button opens dialog to choose Regular or Fast target mode" + - "Discard button shows confirmation dialog before discarding" + - "Iteration count and history are displayed" + - "Storybook stories cover: first iteration, mid-iteration, max-iterations-reached, loading states" + tdd: + red: + - "Write test: prototype tab renders feedback input" + - "Write test: submit button sends feedback action" + - "Write test: promote button opens mode selection dialog" + - "Write test: discard button shows confirmation" + - "Write test: iteration count displays correctly" + green: + - "Create prototype-tab.tsx with feedback form, promote/discard actions" + - "Wire server actions for feedback, promote, and discard" + - "Display prototype diff using existing code diff components" + refactor: + - "Create prototype-tab.stories.tsx with all variant states" + - "Extract action handlers into server actions for clean separation" + estimatedEffort: "2h" + + - id: "task-26" + phaseId: "phase-6" + title: "Update drawer tabs visibility for exploration features" + description: "Update computeVisibleTabs in feature-drawer-tabs.tsx to show the 'prototype' tab when lifecycle is Exploring. Hide SDLC-specific tabs (prd-review, tech-decisions, plan) for exploration features. Add the prototype tab key to the tab registry." + state: "Todo" + dependencies: + - "task-25" + acceptanceCriteria: + - "computeVisibleTabs includes 'prototype' when lifecycle is 'exploring'" + - "SDLC-specific tabs are not shown for exploration features" + - "Tab renders PrototypeTab component when selected" + - "Existing tab visibility logic for regular/fast features is unchanged" + tdd: + red: + - "Write test: computeVisibleTabs returns 'prototype' for exploring lifecycle" + - "Write test: computeVisibleTabs does not return 'prd-review' for exploring lifecycle" + - "Write test: computeVisibleTabs returns 'prd-review' for requirements lifecycle (no regression)" + green: + - "Add prototype tab key to tab registry" + - "Add exploring lifecycle check in computeVisibleTabs" + - "Map prototype tab key to PrototypeTab component" + refactor: + - "Ensure tab ordering is logical (overview, prototype, activity, log)" + estimatedEffort: "30min" + + - id: "task-27" + phaseId: "phase-6" + title: "Wire Web UI server actions for exploration feedback, promote, and discard" + description: "Create or extend server actions to handle exploration-specific operations: submitFeedback (resumes graph with iterate action), promoteExploration (calls PromoteExplorationUseCase), and discardExploration (calls delete use case). These server actions connect the Web UI to the application layer." + state: "Todo" + dependencies: + - "task-16" + - "task-17" + acceptanceCriteria: + - "submitFeedback server action resumes the exploration graph with iterate payload" + - "promoteExploration server action calls PromoteExplorationUseCase" + - "discardExploration server action calls delete/discard use case" + - "Server actions return appropriate response types for UI rendering" + - "Server actions validate feature mode before executing" + tdd: + red: + - "Write test: submitFeedback calls resume with correct payload" + - "Write test: promoteExploration calls use case with correct target mode" + - "Write test: discardExploration calls delete use case" + - "Write test: server actions reject operations on non-exploration features" + green: + - "Create server action functions in appropriate location" + - "Wire to use cases and process service via DI" + - "Return typed responses" + refactor: + - "Follow existing server action patterns for consistency" + estimatedEffort: "1h" + + # ============================================================ + # Phase 7: Settings Integration + Polish + # ============================================================ + + - id: "task-28" + phaseId: "phase-7" + title: "Replace defaultFastMode with defaultMode in settings" + description: "Update WorkflowConfig in settings.tsp to replace defaultFastMode: boolean with defaultMode: string accepting 'regular', 'fast', or 'exploration'. Update settings defaults factory, settings mapper, and all consumers. Default to 'fast' to preserve backward compatibility." + state: "Todo" + dependencies: + - "task-18" + - "task-23" + acceptanceCriteria: + - "WorkflowConfig in settings.tsp has defaultMode instead of defaultFastMode" + - "Default value is 'fast' to preserve backward compatibility" + - "Settings defaults factory returns mode='fast' as default" + - "Settings migration maps defaultFastMode=true to defaultMode='fast'" + - "CLI getWorkflowDefaults reads defaultMode" + - "Web UI mode selector defaults to settings preference" + - "Existing settings with defaultFastMode=true migrate correctly" + tdd: + red: + - "Write test: settings factory returns defaultMode='fast' by default" + - "Write test: settings with defaultFastMode=true migrate to defaultMode='fast'" + - "Write test: settings with defaultFastMode=false migrate to defaultMode='regular'" + green: + - "Update settings.tsp with defaultMode field" + - "Run pnpm tsp:compile" + - "Update settings-defaults.factory.ts" + - "Create settings migration for the boolean-to-string change" + - "Update all consumers" + refactor: + - "Remove dead code referencing defaultFastMode" + estimatedEffort: "1h" + + - id: "task-29" + phaseId: "phase-7" + title: "Add explorationMaxIterations setting" + description: "Add explorationMaxIterations optional integer field to WorkflowConfig in settings.tsp with default 10. Wire it through settings service so the exploration graph reads it as maxIterations. Update CLI and Web UI to respect this setting." + state: "Todo" + dependencies: + - "task-28" + acceptanceCriteria: + - "WorkflowConfig has explorationMaxIterations?: int32 = 10" + - "Settings defaults factory includes explorationMaxIterations=10" + - "Exploration graph receives maxIterations from settings" + - "CreateFeatureUseCase passes maxIterations to feature entity on creation" + - "Users can override via settings" + tdd: + red: + - "Write test: settings factory returns explorationMaxIterations=10 by default" + - "Write test: creating exploration feature sets maxIterations from settings" + green: + - "Add explorationMaxIterations to settings.tsp" + - "Update settings defaults factory" + - "Wire maxIterations through CreateFeatureUseCase to feature entity" + refactor: + - "Verify maxIterations is accessible from the exploration graph nodes" + estimatedEffort: "30min" + +totalEstimate: "24h" + +openQuestions: [] + +content: | + ## Summary + + This task breakdown implements exploration/prototyping mode across 29 tasks in 7 phases. + The work follows a strict dependency chain: domain types must exist before persistence can + map them, persistence must work before the agent graph can read/write features, the graph + must exist before use cases can spawn it, and use cases must exist before CLI/Web UI can + call them. + + Phase 1 establishes the TypeSpec foundation — FeatureMode enum, Exploring lifecycle state, + and iteration fields on the Feature entity. Phase 2 migrates the database from boolean fast + to enum mode and updates the mapper. Phase 3 builds the exploration agent graph with its + prototype-generate and apply-feedback nodes, prompt builders, state channels, and worker + routing. Phase 4 creates the application use cases: extending CreateFeatureUseCase for + exploration mode, creating PromoteExplorationUseCase for mode transitions, and extending + delete logic for exploration cleanup. Phase 5 adds CLI commands: --explore flag, feedback + subcommand, and promote subcommand. Phase 6 builds the Web UI: mode selector component, + canvas node styling, prototype review drawer tab, and server actions. Phase 7 integrates + settings: replacing defaultFastMode with defaultMode and adding explorationMaxIterations. + + Every code task follows TDD: write a failing test first (RED), implement the minimum to + pass (GREEN), then clean up (REFACTOR). The total estimated effort is 24 hours of + focused development work. diff --git a/src/presentation/cli/commands/feat/feedback.command.ts b/src/presentation/cli/commands/feat/feedback.command.ts new file mode 100644 index 000000000..13ccf40bc --- /dev/null +++ b/src/presentation/cli/commands/feat/feedback.command.ts @@ -0,0 +1,83 @@ +/** + * Feature Feedback Command + * + * Sends feedback on an exploration prototype to iterate on the design. + * Resumes the interrupted exploration agent graph with the provided feedback. + * + * Usage: + * shep feat feedback + */ + +import { Command } from 'commander'; +import { container } from '@/infrastructure/di/container.js'; +import type { IFeatureRepository } from '@/application/ports/output/repositories/feature-repository.interface.js'; +import type { IAgentRunRepository } from '@/application/ports/output/agents/agent-run-repository.interface.js'; +import { RejectAgentRunUseCase } from '@/application/use-cases/agents/reject-agent-run.use-case.js'; +import { FeatureMode, SdlcLifecycle } from '@/domain/generated/output.js'; +import { colors, messages } from '../../ui/index.js'; +import { getCliI18n } from '../../i18n.js'; + +export function createFeedbackCommand(): Command { + const t = getCliI18n().t; + return new Command('feedback') + .description(t('cli:commands.feat.feedback.description')) + .argument('', t('cli:commands.feat.feedback.idArgument')) + .argument('', t('cli:commands.feat.feedback.feedbackArgument')) + .action(async (featureId: string, feedback: string) => { + try { + const featureRepo = container.resolve('IFeatureRepository'); + const runRepo = container.resolve('IAgentRunRepository'); + + const feature = + (await featureRepo.findById(featureId)) ?? (await featureRepo.findByIdPrefix(featureId)); + if (!feature) { + throw new Error(`Feature not found: ${featureId}`); + } + + if ( + feature.mode !== FeatureMode.Exploration || + feature.lifecycle !== SdlcLifecycle.Exploring + ) { + messages.error( + t('cli:commands.feat.feedback.notExploration', { + name: feature.name, + mode: feature.mode, + lifecycle: feature.lifecycle, + }) + ); + process.exitCode = 1; + return; + } + + if (!feature.agentRunId) { + throw new Error(`Feature "${feature.name}" has no agent run`); + } + + const run = await runRepo.findById(feature.agentRunId); + if (!run) { + throw new Error(`Agent run not found: ${feature.agentRunId}`); + } + + const rejectUseCase = container.resolve(RejectAgentRunUseCase); + const result = await rejectUseCase.execute(run.id, feedback); + + if (!result.rejected) { + throw new Error(result.reason); + } + + messages.newline(); + messages.success(t('cli:commands.feat.feedback.feedbackSubmitted', { name: feature.name })); + console.log( + ` ${colors.muted(t('cli:commands.feat.feedback.iterationLabel'))} ${result.iteration}` + ); + console.log( + ` ${colors.muted(t('cli:commands.feat.feedback.agentLabel'))} ${t('cli:commands.feat.feedback.agentIterating')}` + ); + messages.newline(); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + messages.error(t('cli:commands.feat.feedback.failedToSubmit'), err); + process.exitCode = 1; + } + }); +} diff --git a/src/presentation/cli/commands/feat/index.ts b/src/presentation/cli/commands/feat/index.ts index d16825617..3488b1d5a 100644 --- a/src/presentation/cli/commands/feat/index.ts +++ b/src/presentation/cli/commands/feat/index.ts @@ -25,6 +25,8 @@ import { createLogsCommand } from './logs.command.js'; import { createAdoptCommand } from './adopt.command.js'; import { createArchiveCommand } from './archive.command.js'; import { createUnarchiveCommand } from './unarchive.command.js'; +import { createFeedbackCommand } from './feedback.command.js'; +import { createPromoteCommand } from './promote.command.js'; /** * Create the feat command group @@ -44,5 +46,7 @@ export function createFeatCommand(): Command { .addCommand(createLogsCommand()) .addCommand(createAdoptCommand()) .addCommand(createArchiveCommand()) - .addCommand(createUnarchiveCommand()); + .addCommand(createUnarchiveCommand()) + .addCommand(createFeedbackCommand()) + .addCommand(createPromoteCommand()); } diff --git a/src/presentation/cli/commands/feat/new.command.ts b/src/presentation/cli/commands/feat/new.command.ts index d72a30ff7..f61f9e839 100644 --- a/src/presentation/cli/commands/feat/new.command.ts +++ b/src/presentation/cli/commands/feat/new.command.ts @@ -17,7 +17,7 @@ import { join, resolve } from 'node:path'; import { container } from '@/infrastructure/di/container.js'; import { CreateFeatureUseCase } from '@/application/use-cases/features/create/create-feature.use-case.js'; import type { ApprovalGates } from '@/domain/generated/output.js'; -import { SdlcLifecycle } from '@/domain/generated/output.js'; +import { SdlcLifecycle, FeatureMode } from '@/domain/generated/output.js'; import type { IFeatureRepository } from '@/application/ports/output/repositories/feature-repository.interface.js'; import { colors, messages, spinner } from '../../ui/index.js'; import { getCliI18n } from '../../i18n.js'; @@ -36,6 +36,7 @@ interface NewOptions { allowAll?: boolean; parent?: string; fast?: boolean; + explore?: boolean; pending?: boolean; model?: string; attach?: string[]; @@ -57,7 +58,7 @@ interface WorkflowDefaults { allowPlan: boolean; allowMerge: boolean; push: boolean; - fast: boolean; + defaultMode: FeatureMode; } function getWorkflowDefaults(): WorkflowDefaults { @@ -68,7 +69,7 @@ function getWorkflowDefaults(): WorkflowDefaults { allowPlan: false, allowMerge: false, push: false, - fast: true, + defaultMode: FeatureMode.Fast, }; } const settings = getSettings(); @@ -79,7 +80,7 @@ function getWorkflowDefaults(): WorkflowDefaults { allowPlan: gates.allowPlan, allowMerge: gates.allowMerge, push: gates.pushOnImplementationComplete, - fast: settings.workflow.defaultFastMode, + defaultMode: (settings.workflow.defaultMode as FeatureMode) ?? FeatureMode.Fast, }; } @@ -103,6 +104,7 @@ export function createNewCommand(): Command { .option('--pending', t('cli:commands.feat.new.pendingOption')) .option('--fast', t('cli:commands.feat.new.fastOption')) .option('--no-fast', t('cli:commands.feat.new.noFastOption')) + .option('--explore', t('cli:commands.feat.new.exploreOption')) .option('--model ', t('cli:commands.feat.new.modelOption')) .option('--no-rebase', t('cli:commands.feat.new.noRebaseOption')) .option('--inject-skills', t('cli:commands.feat.new.injectSkillsOption')) @@ -163,7 +165,20 @@ export function createNewCommand(): Command { } } - const fast = options.fast ?? defaults.fast; + // Validate mutually exclusive mode flags + if (options.explore && options.fast) { + messages.error(t('cli:commands.feat.new.exploreAndFastConflict')); + process.exitCode = 1; + return; + } + + const mode = options.explore + ? FeatureMode.Exploration + : options.fast + ? FeatureMode.Fast + : options.fast === false + ? FeatureMode.Regular + : defaults.defaultMode; const result = await spinner(t('cli:commands.feat.new.spinnerText'), () => useCase.execute({ @@ -174,7 +189,7 @@ export function createNewCommand(): Command { openPr, ...(parentId !== undefined && { parentId }), ...(options.pending && { pending: true }), - ...(fast && { fast: true }), + mode, ...(options.model !== undefined && { model: options.model }), ...(attachmentPaths.length > 0 && { attachmentPaths }), ...(options.injectSkills !== undefined && { injectSkills: options.injectSkills }), diff --git a/src/presentation/cli/commands/feat/promote.command.ts b/src/presentation/cli/commands/feat/promote.command.ts new file mode 100644 index 000000000..de032e64d --- /dev/null +++ b/src/presentation/cli/commands/feat/promote.command.ts @@ -0,0 +1,56 @@ +/** + * Feature Promote Command + * + * Promotes an exploration feature to Regular or Fast mode via + * in-place mode transition using PromoteExplorationUseCase. + * + * Usage: + * shep feat promote # Promote to Regular mode + * shep feat promote --fast # Promote to Fast mode + */ + +import { Command } from 'commander'; +import { container } from '@/infrastructure/di/container.js'; +import { PromoteExplorationUseCase } from '@/application/use-cases/features/promote/promote-exploration.use-case.js'; +import { FeatureMode } from '@/domain/generated/output.js'; +import { colors, messages, spinner } from '../../ui/index.js'; +import { getCliI18n } from '../../i18n.js'; + +interface PromoteOptions { + fast?: boolean; +} + +export function createPromoteCommand(): Command { + const t = getCliI18n().t; + return new Command('promote') + .description(t('cli:commands.feat.promote.description')) + .argument('', t('cli:commands.feat.promote.idArgument')) + .option('--fast', t('cli:commands.feat.promote.fastOption')) + .action(async (featureId: string, options: PromoteOptions) => { + try { + const targetMode = options.fast ? FeatureMode.Fast : FeatureMode.Regular; + + const useCase = container.resolve(PromoteExplorationUseCase); + const { feature } = await spinner(t('cli:commands.feat.promote.description'), () => + useCase.execute({ featureId, targetMode }) + ); + + messages.newline(); + messages.success(t('cli:commands.feat.promote.promoted', { name: feature.name })); + console.log( + ` ${colors.muted(t('cli:commands.feat.promote.modeLabel'))} ${feature.mode}` + ); + console.log( + ` ${colors.muted(t('cli:commands.feat.promote.statusLabel'))} ${feature.lifecycle}` + ); + console.log( + ` ${colors.muted(t('cli:commands.feat.promote.agentLabel'))} ${t('cli:commands.feat.promote.agentSpawned', { mode: targetMode })}` + ); + messages.newline(); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + messages.error(t('cli:commands.feat.promote.failedToPromote'), err); + process.exitCode = 1; + } + }); +} diff --git a/src/presentation/web/app/actions/create-feature.ts b/src/presentation/web/app/actions/create-feature.ts index 1735420a6..4ea8721fc 100644 --- a/src/presentation/web/app/actions/create-feature.ts +++ b/src/presentation/web/app/actions/create-feature.ts @@ -3,6 +3,7 @@ import { resolve } from '@/lib/server-container'; import type { CreateFeatureUseCase } from '@shepai/core/application/use-cases/features/create/create-feature.use-case'; import type { Feature } from '@shepai/core/domain/generated/output'; +import { type FeatureMode } from '@shepai/core/domain/generated/output'; import { composeUserInput } from './compose-user-input'; interface Attachment { @@ -30,8 +31,8 @@ interface CreateFeatureInput { push?: boolean; openPr?: boolean; parentId?: string; - /** When true, skip SDLC phases and implement directly from the prompt. */ - fast?: boolean; + /** Execution mode: Regular (full SDLC), Fast (direct implementation), or Exploration (iterative prototyping). */ + mode?: FeatureMode; /** When true, create the feature in pending state (no agent spawned). */ pending?: boolean; /** Fork repo and create PR to upstream at merge time. */ @@ -66,7 +67,7 @@ export async function createFeature( push, openPr, parentId, - fast, + mode, pending, forkAndPr, commitSpecs, @@ -106,7 +107,7 @@ export async function createFeature( openPr: openPr ?? false, ...(parentId ? { parentId } : {}), description, - ...(fast ? { fast } : {}), + ...(mode ? { mode } : {}), ...(pending ? { pending } : {}), ...(forkAndPr != null ? { forkAndPr } : {}), ...(commitSpecs != null ? { commitSpecs } : {}), @@ -131,7 +132,7 @@ export async function createFeature( push: push ?? false, openPr: openPr ?? false, ...(parentId ? { parentId } : {}), - ...(fast ? { fast } : {}), + ...(mode ? { mode } : {}), ...(pending ? { pending } : {}), ...(forkAndPr != null ? { forkAndPr } : {}), ...(commitSpecs != null ? { commitSpecs } : {}), diff --git a/src/presentation/web/app/actions/discard-exploration.ts b/src/presentation/web/app/actions/discard-exploration.ts new file mode 100644 index 000000000..2b2e062ab --- /dev/null +++ b/src/presentation/web/app/actions/discard-exploration.ts @@ -0,0 +1,38 @@ +'use server'; + +import { resolve } from '@/lib/server-container'; +import type { DeleteFeatureUseCase } from '@shepai/core/application/use-cases/features/delete-feature.use-case'; +import type { IFeatureRepository } from '@shepai/core/application/ports/output/repositories/feature-repository.interface'; +import { FeatureMode } from '@shepai/core/domain/generated/output'; + +/** + * Discard an exploration feature, cleaning up the worktree and branch. + * Validates the feature is in exploration mode before deleting. + */ +export async function discardExploration( + featureId: string +): Promise<{ discarded: boolean; error?: string }> { + if (!featureId.trim()) { + return { discarded: false, error: 'Feature id is required' }; + } + + try { + const featureRepo = resolve('IFeatureRepository'); + const feature = await featureRepo.findById(featureId); + + if (!feature) { + return { discarded: false, error: 'Feature not found' }; + } + + if (feature.mode !== FeatureMode.Exploration) { + return { discarded: false, error: 'Feature is not in exploration mode' }; + } + + const deleteUseCase = resolve('DeleteFeatureUseCase'); + await deleteUseCase.execute(featureId, { cleanup: true }); + return { discarded: true }; + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Failed to discard exploration'; + return { discarded: false, error: message }; + } +} diff --git a/src/presentation/web/app/actions/get-workflow-defaults.ts b/src/presentation/web/app/actions/get-workflow-defaults.ts index e8b3fe4fb..9da769681 100644 --- a/src/presentation/web/app/actions/get-workflow-defaults.ts +++ b/src/presentation/web/app/actions/get-workflow-defaults.ts @@ -1,6 +1,7 @@ 'use server'; import { getSettings } from '@shepai/core/infrastructure/services/settings.service'; +import { FeatureMode } from '@shepai/core/domain/generated/output'; export interface WorkflowDefaults { approvalGates: { @@ -13,7 +14,7 @@ export interface WorkflowDefaults { ciWatchEnabled: boolean; enableEvidence: boolean; commitEvidence: boolean; - fast: boolean; + defaultMode: FeatureMode; injectSkills: boolean; } @@ -32,7 +33,7 @@ export async function getWorkflowDefaults(): Promise { ciWatchEnabled: workflow.ciWatchEnabled, enableEvidence: workflow.enableEvidence, commitEvidence: workflow.commitEvidence, - fast: workflow.defaultFastMode, + defaultMode: (workflow.defaultMode as FeatureMode) ?? FeatureMode.Fast, injectSkills: workflow.skillInjection?.enabled ?? false, }; } diff --git a/src/presentation/web/app/actions/promote-exploration.ts b/src/presentation/web/app/actions/promote-exploration.ts new file mode 100644 index 000000000..75c552cd5 --- /dev/null +++ b/src/presentation/web/app/actions/promote-exploration.ts @@ -0,0 +1,28 @@ +'use server'; + +import { resolve } from '@/lib/server-container'; +import type { PromoteExplorationUseCase } from '@shepai/core/application/use-cases/features/promote/promote-exploration.use-case'; +import type { Feature, FeatureMode } from '@shepai/core/domain/generated/output'; + +/** + * Promote an exploration feature to Regular or Fast mode. + * Transitions the feature from Exploring lifecycle to the appropriate + * starting state and spawns the new agent graph. + */ +export async function promoteExploration( + featureId: string, + targetMode: FeatureMode.Regular | FeatureMode.Fast +): Promise<{ feature?: Feature; error?: string }> { + if (!featureId.trim()) { + return { error: 'Feature id is required' }; + } + + try { + const useCase = resolve('PromoteExplorationUseCase'); + const result = await useCase.execute({ featureId, targetMode }); + return { feature: result.feature }; + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Failed to promote exploration'; + return { error: message }; + } +} diff --git a/src/presentation/web/app/actions/submit-exploration-feedback.ts b/src/presentation/web/app/actions/submit-exploration-feedback.ts new file mode 100644 index 000000000..4f8599483 --- /dev/null +++ b/src/presentation/web/app/actions/submit-exploration-feedback.ts @@ -0,0 +1,54 @@ +'use server'; + +import { resolve } from '@/lib/server-container'; +import type { RejectAgentRunUseCase } from '@shepai/core/application/use-cases/agents/reject-agent-run.use-case'; +import type { IFeatureRepository } from '@shepai/core/application/ports/output/repositories/feature-repository.interface'; +import { FeatureMode } from '@shepai/core/domain/generated/output'; + +/** + * Submit feedback on an exploration prototype. This resumes the exploration + * graph with the feedback text, triggering the next iteration. Internally + * uses the RejectAgentRunUseCase since exploration feedback follows the + * same interrupt/resume mechanism as approval gate rejections. + */ +export async function submitExplorationFeedback( + featureId: string, + feedback: string +): Promise<{ submitted: boolean; iteration?: number; error?: string }> { + if (!featureId.trim()) { + return { submitted: false, error: 'Feature id is required' }; + } + + if (!feedback.trim()) { + return { submitted: false, error: 'Feedback is required' }; + } + + try { + const featureRepo = resolve('IFeatureRepository'); + const feature = await featureRepo.findById(featureId); + + if (!feature) { + return { submitted: false, error: 'Feature not found' }; + } + + if (feature.mode !== FeatureMode.Exploration) { + return { submitted: false, error: 'Feature is not in exploration mode' }; + } + + if (!feature.agentRunId) { + return { submitted: false, error: 'Feature has no agent run' }; + } + + const rejectUseCase = resolve('RejectAgentRunUseCase'); + const result = await rejectUseCase.execute(feature.agentRunId, feedback); + + if (!result.rejected) { + return { submitted: false, error: result.reason }; + } + + return { submitted: true, iteration: result.iteration }; + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Failed to submit feedback'; + return { submitted: false, error: message }; + } +} diff --git a/src/presentation/web/app/api/agent-events/route.ts b/src/presentation/web/app/api/agent-events/route.ts index 78c0c4369..45e202ab4 100644 --- a/src/presentation/web/app/api/agent-events/route.ts +++ b/src/presentation/web/app/api/agent-events/route.ts @@ -63,6 +63,7 @@ const LIFECYCLE_TO_NODE: Record = { [SdlcLifecycle.Maintain]: 'maintain', [SdlcLifecycle.Blocked]: 'blocked', [SdlcLifecycle.Pending]: 'pending', + [SdlcLifecycle.Exploring]: 'exploring', [SdlcLifecycle.Deleting]: 'blocked', [SdlcLifecycle.AwaitingUpstream]: 'merge', [SdlcLifecycle.Archived]: 'archived', diff --git a/src/presentation/web/app/build-feature-node-data.ts b/src/presentation/web/app/build-feature-node-data.ts index 428139108..29fba61e8 100644 --- a/src/presentation/web/app/build-feature-node-data.ts +++ b/src/presentation/web/app/build-feature-node-data.ts @@ -1,5 +1,5 @@ import type { Feature, AgentRun } from '@shepai/core/domain/generated/output'; -import { AgentRunStatus } from '@shepai/core/domain/generated/output'; +import { AgentRunStatus, FeatureMode } from '@shepai/core/domain/generated/output'; import { deriveNodeState, deriveProgress, @@ -58,7 +58,9 @@ export function buildFeatureNodeData( summary: feature.description, userQuery: feature.userQuery, createdAt: feature.createdAt instanceof Date ? feature.createdAt.getTime() : feature.createdAt, - ...(feature.fast && { fastMode: true }), + ...(feature.mode === FeatureMode.Fast && { fastMode: true }), + mode: feature.mode, + ...(feature.iterationCount > 0 && { iterationCount: feature.iterationCount }), approvalGates: feature.approvalGates, push: feature.push, openPr: feature.openPr, diff --git a/src/presentation/web/app/build-graph-nodes.ts b/src/presentation/web/app/build-graph-nodes.ts index 1cf59ad1b..9c2efd8d2 100644 --- a/src/presentation/web/app/build-graph-nodes.ts +++ b/src/presentation/web/app/build-graph-nodes.ts @@ -1,5 +1,5 @@ import type { Feature, Repository, AgentRun } from '@shepai/core/domain/generated/output'; -import { AgentRunStatus } from '@shepai/core/domain/generated/output'; +import { AgentRunStatus, FeatureMode } from '@shepai/core/domain/generated/output'; import { deriveNodeState, deriveProgress, @@ -203,7 +203,9 @@ function appendFeatureNodes( userQuery: feature.userQuery, createdAt: feature.createdAt instanceof Date ? feature.createdAt.getTime() : feature.createdAt, - ...(feature.fast && { fastMode: true }), + ...(feature.mode === FeatureMode.Fast && { fastMode: true }), + mode: feature.mode, + ...(feature.iterationCount > 0 && { iterationCount: feature.iterationCount }), approvalGates: feature.approvalGates, push: feature.push, openPr: feature.openPr, diff --git a/src/presentation/web/components/common/control-center-drawer/drawer-view.ts b/src/presentation/web/components/common/control-center-drawer/drawer-view.ts index 6c35968b3..35b049e21 100644 --- a/src/presentation/web/components/common/control-center-drawer/drawer-view.ts +++ b/src/presentation/web/components/common/control-center-drawer/drawer-view.ts @@ -13,6 +13,7 @@ export type FeatureTabKey = | 'tech-decisions' | 'product-decisions' | 'merge-review' + | 'prototype' | 'chat'; /** All valid tab key values — used for URL param validation. */ @@ -25,6 +26,7 @@ export const VALID_TAB_KEYS: ReadonlySet = new Set([ 'tech-decisions', 'product-decisions', 'merge-review', + 'prototype', 'chat', ]); @@ -54,6 +56,7 @@ export type DrawerView = /** Derives the initial tab from node lifecycle + state. */ export function deriveInitialTab(node: FeatureNodeData): FeatureTabKey { + if (node.lifecycle === 'exploring') return 'prototype'; if (node.lifecycle === 'requirements' && node.state === 'action-required') return 'prd-review'; if (node.lifecycle === 'implementation' && node.state === 'action-required') return 'tech-decisions'; diff --git a/src/presentation/web/components/common/control-center-drawer/feature-drawer-client.tsx b/src/presentation/web/components/common/control-center-drawer/feature-drawer-client.tsx index bd2e51498..15b8e8ee4 100644 --- a/src/presentation/web/components/common/control-center-drawer/feature-drawer-client.tsx +++ b/src/presentation/web/components/common/control-center-drawer/feature-drawer-client.tsx @@ -10,11 +10,15 @@ import type { PrdApprovalPayload, QuestionSelectionChange, } from '@shepai/core/domain/generated/output'; +import type { FeatureMode } from '@shepai/core/domain/generated/output'; import { approveFeature } from '@/app/actions/approve-feature'; import { resumeFeature } from '@/app/actions/resume-feature'; import { startFeature } from '@/app/actions/start-feature'; import { stopFeature } from '@/app/actions/stop-feature'; import { rejectFeature } from '@/app/actions/reject-feature'; +import { submitExplorationFeedback } from '@/app/actions/submit-exploration-feedback'; +import { promoteExploration } from '@/app/actions/promote-exploration'; +import { discardExploration } from '@/app/actions/discard-exploration'; import type { RejectAttachment } from '@/components/common/drawer-action-bar'; import { getFeatureArtifact } from '@/app/actions/get-feature-artifact'; import { getResearchArtifact } from '@/app/actions/get-research-artifact'; @@ -214,6 +218,9 @@ export function FeatureDrawerClient({ } }, [archiveResetKey]); + // ── Exploration state ──────────────────────────────────────────────── + const [isPrototypeSubmitting, setIsPrototypeSubmitting] = useState(false); + // ── Shared reject state ──────────────────────────────────────────────── const [isRejecting, setIsRejecting] = useState(false); const isRejectingRef = useRef(false); @@ -629,6 +636,75 @@ export function FeatureDrawerClient({ [featureNode, lastSavedPinnedConfig] ); + // ── Exploration handlers ───────────────────────────────────────────── + + const handleSubmitFeedback = useCallback( + async (feedback: string) => { + if (!featureNode?.featureId) return; + setIsPrototypeSubmitting(true); + try { + const result = await submitExplorationFeedback(featureNode.featureId, feedback); + if (!result.submitted) { + toast.error(result.error ?? 'Failed to submit feedback'); + return; + } + toast.success(`Feedback sent — generating iteration ${(result.iteration ?? 0) + 1}`); + window.dispatchEvent( + new CustomEvent('shep:feature-approved', { + detail: { featureId: featureNode.featureId }, + }) + ); + } finally { + setIsPrototypeSubmitting(false); + } + }, + [featureNode] + ); + + const handlePromote = useCallback( + async (targetMode: FeatureMode.Regular | FeatureMode.Fast) => { + if (!featureNode?.featureId) return; + setIsPrototypeSubmitting(true); + try { + const result = await promoteExploration(featureNode.featureId, targetMode); + if (result.error) { + toast.error(result.error); + return; + } + toast.success(`Exploration promoted to ${targetMode === 'Fast' ? 'fast' : 'regular'} mode`); + window.dispatchEvent( + new CustomEvent('shep:feature-approved', { + detail: { featureId: featureNode.featureId }, + }) + ); + } finally { + setIsPrototypeSubmitting(false); + } + }, + [featureNode] + ); + + const handleDiscardExploration = useCallback(async () => { + if (!featureNode?.featureId) return; + setIsPrototypeSubmitting(true); + try { + const result = await discardExploration(featureNode.featureId); + if (!result.discarded) { + toast.error(result.error ?? 'Failed to discard exploration'); + return; + } + toast.success('Exploration discarded'); + window.dispatchEvent( + new CustomEvent('shep:feature-delete-requested', { + detail: { featureId: featureNode.featureId, cleanup: true }, + }) + ); + router.push('/'); + } finally { + setIsPrototypeSubmitting(false); + } + }, [featureNode, router]); + // ── Hooks (always called unconditionally per Rules of Hooks) ────────── const featureActionsInput = @@ -942,6 +1018,10 @@ export function FeatureDrawerClient({ : undefined } continuationActionsDisabled={pinnedConfigSaving} + onSubmitFeedback={handleSubmitFeedback} + onPromote={handlePromote} + onDiscardExploration={handleDiscardExploration} + isPrototypeSubmitting={isPrototypeSubmitting} interactiveAgentEnabled={interactiveAgentEnabled} onRetry={handleRetry} onStop={handleStop} diff --git a/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.stories.tsx b/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.stories.tsx index 9db608686..031b03383 100644 --- a/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.stories.tsx +++ b/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.stories.tsx @@ -4,6 +4,7 @@ import { within, userEvent, fn, expect } from '@storybook/test'; import { FeatureCreateDrawer } from './feature-create-drawer'; import type { FeatureCreatePayload, RepositoryOption } from './feature-create-drawer'; import type { WorkflowDefaults } from '@/app/actions/get-workflow-defaults'; +import { FeatureMode } from '@shepai/core/domain/generated/output'; import { Button } from '@/components/ui/button'; import { DrawerCloseGuardProvider } from '@/hooks/drawer-close-guard'; @@ -347,7 +348,7 @@ const SAMPLE_WORKFLOW_DEFAULTS: WorkflowDefaults = { ciWatchEnabled: true, enableEvidence: true, commitEvidence: false, - fast: true, + defaultMode: FeatureMode.Fast, injectSkills: false, }; diff --git a/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.tsx b/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.tsx index dfcf8d8ab..c806672e3 100644 --- a/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.tsx +++ b/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.tsx @@ -6,7 +6,6 @@ import { PaperclipIcon, ChevronsUpDown, CheckIcon, - Zap, Clock, FolderPlus, Loader2, @@ -36,7 +35,9 @@ import { Separator } from '@/components/ui/separator'; import { pickFolder } from '@/components/common/add-repository-button/pick-folder'; import { ReactFileManagerDialog } from '@/components/common/react-file-manager-dialog'; import { useFeatureFlags } from '@/hooks/feature-flags-context'; +import { FeatureMode } from '@shepai/core/domain/generated/output'; import { addRepository } from '@/app/actions/add-repository'; +import { ModeSelector } from './mode-selector'; import { pickFiles } from './pick-files'; export type { FileAttachment } from '@shepai/core/infrastructure/services/file-dialog.service'; @@ -81,8 +82,8 @@ export interface FeatureCreatePayload { enableEvidence: boolean; commitEvidence: boolean; parentId?: string; - /** When true, skip SDLC phases and implement directly from the prompt. */ - fast: boolean; + /** Execution mode: Regular (full SDLC), Fast (direct implementation), or Exploration (iterative prototyping). */ + mode: FeatureMode; /** When true, create the feature in pending state (no agent spawned). */ pending?: boolean; /** Fork repo and create PR to upstream at merge time. */ @@ -228,7 +229,7 @@ export function FeatureCreateDrawer({ const defaultCiWatch = workflowDefaults?.ciWatchEnabled !== false; const defaultEnableEvidence = workflowDefaults?.enableEvidence ?? false; const defaultCommitEvidence = workflowDefaults?.commitEvidence ?? false; - const defaultFast = workflowDefaults?.fast !== false; + const defaultMode = workflowDefaults?.defaultMode ?? FeatureMode.Fast; const [description, setDescription] = useState(initialDescription ?? ''); @@ -247,7 +248,7 @@ export function FeatureCreateDrawer({ const [enableEvidence, setEnableEvidence] = useState(defaultEnableEvidence); const [commitEvidence, setCommitEvidence] = useState(defaultCommitEvidence); const [parentId, setParentId] = useState(undefined); - const [fast, setFast] = useState(defaultFast); + const [mode, setMode] = useState(defaultMode); const [pending, setPending] = useState(false); const [forkAndPr, setForkAndPr] = useState(false); const [commitSpecs, setCommitSpecs] = useState(true); @@ -277,7 +278,7 @@ export function FeatureCreateDrawer({ setCiWatchEnabled(workflowDefaults.ciWatchEnabled !== false); setEnableEvidence(workflowDefaults.enableEvidence); setCommitEvidence(workflowDefaults.commitEvidence); - setFast(workflowDefaults.fast !== false); + setMode(workflowDefaults.defaultMode ?? FeatureMode.Fast); setInjectSkills(workflowDefaults.injectSkills ?? false); } }, [workflowDefaults]); @@ -335,7 +336,7 @@ export function FeatureCreateDrawer({ setParentId(undefined); setSelectedRepoPath(validRepoPath || undefined); setLocalRepos(repositories ?? []); - setFast(defaultFast); + setMode(defaultMode); setPending(false); setForkAndPr(false); setCommitSpecs(true); @@ -352,7 +353,7 @@ export function FeatureCreateDrawer({ defaultEnableEvidence, defaultCiWatch, defaultCommitEvidence, - defaultFast, + defaultMode, validRepoPath, repositories, ]); @@ -511,7 +512,7 @@ export function FeatureCreateDrawer({ ciWatchEnabled, enableEvidence, commitEvidence, - fast, + mode, forkAndPr, commitSpecs, rebaseBeforeBranch, @@ -536,7 +537,7 @@ export function FeatureCreateDrawer({ enableEvidence, ciWatchEnabled, commitEvidence, - fast, + mode, forkAndPr, commitSpecs, rebaseBeforeBranch, @@ -827,28 +828,7 @@ export function FeatureCreateDrawer({ {t('createDrawer.pendingModeDescription')} - - -
- - -
-
- - {t('createDrawer.fastModeDescription')} - -
+ + + + {t('createDrawer.toggleAllApprovalGates')} + + - {/* Select all shortcut */} - - - - - - {t('createDrawer.toggleAllApprovalGates')} - - - + )} {/* Evidence row */}
diff --git a/src/presentation/web/components/common/feature-create-drawer/mode-selector.stories.tsx b/src/presentation/web/components/common/feature-create-drawer/mode-selector.stories.tsx new file mode 100644 index 000000000..60dfd5d3b --- /dev/null +++ b/src/presentation/web/components/common/feature-create-drawer/mode-selector.stories.tsx @@ -0,0 +1,42 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { ModeSelector } from './mode-selector'; +import { FeatureMode } from '@shepai/core/domain/generated/output'; + +const meta: Meta = { + title: 'Drawers/Feature/ModeSelector', + component: ModeSelector, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, +}; + +export default meta; + +type Story = StoryObj; + +function InteractiveModeSelector({ initial = FeatureMode.Fast }: { initial?: FeatureMode }) { + const [mode, setMode] = useState(initial); + return ; +} + +export const Regular: Story = { + render: () => , +}; + +export const Fast: Story = { + render: () => , +}; + +export const Exploration: Story = { + render: () => , +}; + +export const Disabled: Story = { + args: { + value: FeatureMode.Fast, + onChange: () => undefined, + disabled: true, + }, +}; diff --git a/src/presentation/web/components/common/feature-create-drawer/mode-selector.tsx b/src/presentation/web/components/common/feature-create-drawer/mode-selector.tsx new file mode 100644 index 000000000..cd1d05ed4 --- /dev/null +++ b/src/presentation/web/components/common/feature-create-drawer/mode-selector.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { ClipboardList, Zap, FlaskConical } from 'lucide-react'; +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { FeatureMode } from '@shepai/core/domain/generated/output'; + +const MODE_OPTIONS = [ + { + value: FeatureMode.Regular, + icon: ClipboardList, + label: 'Regular', + description: 'Full SDLC — requirements, research, planning, implementation, and review.', + }, + { + value: FeatureMode.Fast, + icon: Zap, + label: 'Fast', + description: 'Direct implementation — skip SDLC phases, go straight to code.', + }, + { + value: FeatureMode.Exploration, + icon: FlaskConical, + label: 'Explore', + description: 'Iterative prototyping — generate quick prototypes and iterate with feedback.', + }, +] as const; + +export interface ModeSelectorProps { + value: FeatureMode; + onChange: (mode: FeatureMode) => void; + disabled?: boolean; +} + +export function ModeSelector({ value, onChange, disabled }: ModeSelectorProps) { + return ( + + { + // ToggleGroup emits empty string when clicking the already-selected item — ignore it + if (v) onChange(v as FeatureMode); + }} + disabled={disabled} + aria-label="Feature mode" + data-testid="mode-selector" + > + {MODE_OPTIONS.map((opt) => ( + + + + + {opt.label} + + + {opt.description} + + ))} + + + ); +} diff --git a/src/presentation/web/components/common/feature-drawer-tabs/feature-drawer-tabs.tsx b/src/presentation/web/components/common/feature-drawer-tabs/feature-drawer-tabs.tsx index 0c68e1f9c..b4cc8abeb 100644 --- a/src/presentation/web/components/common/feature-drawer-tabs/feature-drawer-tabs.tsx +++ b/src/presentation/web/components/common/feature-drawer-tabs/feature-drawer-tabs.tsx @@ -20,6 +20,7 @@ import { RotateCcw, Zap, Layers, + FlaskConical, } from 'lucide-react'; import type { NotificationEvent } from '@shepai/core/domain/generated/output'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; @@ -45,10 +46,12 @@ import { ProductDecisionsSummary } from '@/components/common/product-decisions-s import { MergeReview } from '@/components/common/merge-review'; import { DrawerActionBar } from '@/components/common/drawer-action-bar'; import type { RejectAttachment } from '@/components/common/drawer-action-bar'; +import type { FeatureMode } from '@shepai/core/domain/generated/output'; import { OverviewTab } from './overview-tab'; import { ActivityTab } from './activity-tab'; import { LogTab } from './log-tab'; import { PlanTab } from './plan-tab'; +import { PrototypeTab } from './prototype-tab'; import { ChatTab } from '@/components/features/chat/ChatTab'; import { useFeatureLogs } from '@/hooks/use-feature-logs'; import { useTabDataFetch } from './use-tab-data-fetch'; @@ -70,6 +73,7 @@ interface TabDef { /** All possible tabs in display order. */ const ALL_TABS: TabDef[] = [ { key: 'overview', label: 'Overview', icon: LayoutDashboard }, + { key: 'prototype', label: 'Prototype', icon: FlaskConical }, { key: 'activity', label: 'Activity', icon: Activity }, { key: 'log', label: 'Log', icon: ScrollText }, { key: 'plan', label: 'Plan', icon: Map }, @@ -85,7 +89,17 @@ function computeVisibleTabs( node: FeatureNodeData, interactiveAgentEnabled = true ): FeatureTabKey[] { - const tabs: FeatureTabKey[] = ['overview', 'activity']; + const tabs: FeatureTabKey[] = ['overview']; + + // Exploration mode: show prototype tab, skip SDLC-specific tabs + if (node.lifecycle === 'exploring') { + tabs.push('prototype', 'activity'); + if (node.hasAgentRun) tabs.push('log'); + if (interactiveAgentEnabled) tabs.push('chat'); + return tabs; + } + + tabs.push('activity'); if (node.hasAgentRun) { tabs.push('log'); @@ -173,6 +187,12 @@ export interface FeatureDrawerTabsProps { pinnedConfig?: FeatureDrawerPinnedConfig; continuationActionsDisabled?: boolean; + // Exploration / Prototype + onSubmitFeedback?: (feedback: string) => Promise; + onPromote?: (targetMode: FeatureMode.Regular | FeatureMode.Fast) => Promise; + onDiscardExploration?: () => Promise; + isPrototypeSubmitting?: boolean; + // Interactive agent /** When false, the Chat tab is hidden from the tab bar (FR-17). Defaults to true. */ interactiveAgentEnabled?: boolean; @@ -235,6 +255,10 @@ export function FeatureDrawerTabs({ pinnedConfig, continuationActionsDisabled = false, sseEvents, + onSubmitFeedback, + onPromote, + onDiscardExploration, + isPrototypeSubmitting, interactiveAgentEnabled = true, onRetry, onStop, @@ -602,6 +626,19 @@ export function FeatureDrawerTabs({ /> + {/* Prototype tab — visible for exploration features */} + {visibleTabs.includes('prototype') ? ( + + + + ) : null} + = { + title: 'Drawers/Feature/PrototypeTab', + component: PrototypeTab, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, +}; + +export default meta; + +type Story = StoryObj; + +const baseExplorationData: FeatureNodeData = { + name: 'Workspace grouping prototype', + description: 'Explore different UI approaches for grouping repos into workspaces', + featureId: 'abc123', + lifecycle: 'exploring', + state: 'action-required', + progress: 0, + repositoryPath: '/home/user/project', + branch: 'feat/explore-workspace-grouping', + mode: FeatureMode.Exploration, + iterationCount: 1, +}; + +/** First iteration — prototype just generated, awaiting feedback. */ +export const FirstIteration: Story = { + args: { + data: { ...baseExplorationData, iterationCount: 1 }, + onSubmitFeedback: fn(), + onPromote: fn(), + onDiscard: fn(), + }, +}; + +/** Mid-iteration — user has given multiple rounds of feedback. */ +export const MidIteration: Story = { + args: { + data: { ...baseExplorationData, iterationCount: 5 }, + onSubmitFeedback: fn(), + onPromote: fn(), + onDiscard: fn(), + }, +}; + +/** Generating — the agent is currently producing a prototype. */ +export const Generating: Story = { + args: { + data: { ...baseExplorationData, state: 'running', iterationCount: 3 }, + onSubmitFeedback: fn(), + onPromote: fn(), + onDiscard: fn(), + }, +}; + +/** Submitting feedback — loading state while feedback is being sent. */ +export const Submitting: Story = { + args: { + data: { ...baseExplorationData, iterationCount: 2 }, + onSubmitFeedback: fn(), + onPromote: fn(), + onDiscard: fn(), + isSubmitting: true, + }, +}; + +/** Zero iterations — just created, no prototype yet. */ +export const ZeroIterations: Story = { + args: { + data: { ...baseExplorationData, state: 'running', iterationCount: 0 }, + onSubmitFeedback: fn(), + onPromote: fn(), + onDiscard: fn(), + }, +}; diff --git a/src/presentation/web/components/common/feature-drawer-tabs/prototype-tab.tsx b/src/presentation/web/components/common/feature-drawer-tabs/prototype-tab.tsx new file mode 100644 index 000000000..8e825d1ce --- /dev/null +++ b/src/presentation/web/components/common/feature-drawer-tabs/prototype-tab.tsx @@ -0,0 +1,205 @@ +'use client'; + +import { useState, useCallback } from 'react'; +import { FlaskConical, ArrowUpRight, Trash2, Loader2, Send } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { FeatureMode } from '@shepai/core/domain/generated/output'; +import type { FeatureNodeData } from '@/components/common/feature-node'; + +export interface PrototypeTabProps { + data: FeatureNodeData; + onSubmitFeedback?: (feedback: string) => Promise; + onPromote?: (targetMode: FeatureMode.Regular | FeatureMode.Fast) => Promise; + onDiscard?: () => Promise; + isSubmitting?: boolean; +} + +export function PrototypeTab({ + data, + onSubmitFeedback, + onPromote, + onDiscard, + isSubmitting = false, +}: PrototypeTabProps) { + const [feedback, setFeedback] = useState(''); + const [discardDialogOpen, setDiscardDialogOpen] = useState(false); + const [promoteDialogOpen, setPromoteDialogOpen] = useState(false); + + const handleSubmitFeedback = useCallback(async () => { + if (!feedback.trim() || !onSubmitFeedback) return; + await onSubmitFeedback(feedback.trim()); + setFeedback(''); + }, [feedback, onSubmitFeedback]); + + const handlePromote = useCallback( + async (targetMode: FeatureMode.Regular | FeatureMode.Fast) => { + setPromoteDialogOpen(false); + await onPromote?.(targetMode); + }, + [onPromote] + ); + + const handleDiscard = useCallback(async () => { + setDiscardDialogOpen(false); + await onDiscard?.(); + }, [onDiscard]); + + const isWaitingFeedback = data.state === 'action-required' && data.lifecycle === 'exploring'; + const isGenerating = data.state === 'running' && data.lifecycle === 'exploring'; + + return ( +
+ {/* Status + iteration badge */} +
+ + Exploration Prototype + {(data.iterationCount ?? 0) > 0 && ( + + Iteration {data.iterationCount} + + )} + {isGenerating ? ( + + + Generating... + + ) : null} + {isWaitingFeedback ? ( + + Awaiting feedback + + ) : null} +
+ + + + {/* Feedback input section */} +
+ +