// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 avtc /** * Phase progression — pure pointer math over the ordered feature-flow phases. * * Models the agent's position in a fixed phase sequence * (design → plan → implement → verify → review → uat → finish) as a single * pointer: {@link PhaseProgressionState.currentPhase}. Phase status is NEVER * stored — it is DERIVED from the pointer plus the feature's `completedAt` * (see {@link isPhaseDone} / {@link isPhaseActive}): * - every phase before the pointer is `done` (moved through or skipped), * - the pointer phase is `in-progress`, * - every phase after the pointer is `pending`. * * This module owns ONLY pointer movement (setCurrentPhase, onInputText, * recordDoc) + status derivation. Sub-loop-aware routing policy (which phase * comes next per settings, review/UAT skipping) lives in * {@link WorkflowRouter} (workflow-router.ts), which advances via * {@link setCurrentPhase} on this progression. * * Moving forward implicitly completes every jumped-over phase (derived). * Moving backward implicitly resets the target-onward phases (derived). No status * map is kept, so it can never drift from the pointer. */ import type { CustomEntry, SessionEntry } from "@earendil-works/pi-coding-agent"; import { log } from "../log.js"; import { DESIGN_DOC_DIRS, FF_TASK_PLANS_DIR } from "../state/artifact-paths.js"; /** The ordered feature-flow phases. */ export const WORKFLOW_PHASES = ["design", "plan", "implement", "verify", "review", "uat", "finish"] as const; /** A single phase in the sequence. */ export type Phase = (typeof WORKFLOW_PHASES)[number]; /** Lifecycle status of one phase (derived, never stored on disk). */ export type PhaseStatus = "pending" | "in-progress" | "done"; /** Phases that may NOT bootstrap a brand-new workflow from idle. */ export const FRESH_START_BLOCKED: ReadonlySet = new Set(["verify", "review", "finish"]); /** Skill name → phase. A skill invocation drives progression to its phase. */ export const SKILL_TO_PHASE: Record = { "ff-design": "design", "ff-plan": "plan", "ff-implement": "implement", "ff-verify": "verify", "ff-review": "review", "ff-design-review": "design", "ff-plan-review": "plan", "ff-finish": "finish", }; /** Phase → canonical skill name (inverse of the driver subset). */ export const PHASE_TO_SKILL: Record = { design: "ff-design", plan: "ff-plan", implement: "ff-implement", verify: "ff-verify", review: "ff-review", uat: "ff-review", finish: "ff-finish", }; /** Resolve a skill name for a phase, falling back to the phase name. */ export function resolveSkillForPhase(phase: string): string { return PHASE_TO_SKILL[phase as Phase] ?? phase; } /** Extract a skill name from a `/skill:name` or `` line; null otherwise. */ export function parseSkillName(line: string): string | null { const slash = line.match(/^\s*\/skill:([^\s]+)/); if (slash) return slash[1]; const xml = line.match(/^\s* isInsideDir(filePath, d))) { return this.recordDoc("design", filePath); } if (TASK_PLAN_DOC.test(filePath) && isInsideDir(filePath, FF_TASK_PLANS_DIR)) { return this.recordDoc("plan", filePath); } return false; } /** Restore the most recent phase-progression snapshot from a session branch. */ static reconstructFromBranch(branch: SessionEntry[]): PhaseProgressionState | null { let latest: PhaseProgressionState | null = null; for (const entry of branch) { if (entry.type !== "custom") continue; if ((entry as CustomEntry).customType !== PHASE_PROGRESSION_ENTRY_TYPE) continue; const data = (entry as CustomEntry).data; if (data && typeof data === "object") latest = structuredClone(data); } return latest; } }