/** * morph — Agent Orchestration Layer for pi * * 5-stage pipeline: spark → plan → work → review → ship * * TUI display shows live phase dashboard, task tracker, * agent activity, and token costs during execution. */ import * as fs from "node:fs"; import * as path from "node:path"; import { spawn } from "node:child_process"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Text } from "@earendil-works/pi-tui"; import { Blackboard, getMorphDir, type ArchivedMorphState } from "./core/blackboard.js"; import { detectFileTargetOverlaps, formatDAG, formatProgress, waveGroups, estimatePhaseTokens } from "./core/engine.js"; import { formatTokens } from "./core/tokenizer.js"; import { executeSparkFlow } from "./flows/spark.js"; import { executePlanFlow } from "./flows/plan.js"; import { executeWorkFlow } from "./flows/work.js"; import { executeReviewFlow } from "./flows/review.js"; import { executeShipFlow } from "./flows/ship.js"; import { SPARK_AGENTS, PLAN_AGENTS, WORK_AGENTS, REVIEW_AGENTS, SHIP_AGENTS, } from "./core/agent-runner.js"; import { type PipelineDisplay, type TaskDisplay, type AgentActivity, type SubagentActivity, type FileActivity, type FileCollision, type PhaseContext, buildPipelineProgressWidget, buildTaskTracker, buildStatusBar, } from "./tui/display.js"; import { startMorphServer, serverEvents } from "./server/server.js"; import type { Server } from "node:http"; import type { MorphState, PlanOutput, ReviewOutput, SparkOutput } from "./schemas/contracts.js"; import { renderSkillProfiles } from "./core/skill-profiles.js"; import { summarizeRecoveryState, diagnoseRecoveryState, persistRecoveryReport, shouldAutoRecoverWithinWork, clearAutoRecoverableTaskResult, clearWorkResultBranch, prepareExplicitWorkRecovery, restoreTrustedRegressionSnapshot, } from "./core/recovery.js"; import { escapeHtml, renderHtmlList, renderMarkdownLite, renderBrowserGatePage, buildWorkSpecHtml, buildSparkApprovalHtml, buildReviewApprovalHtml, buildReviewExceptionHtml, assessReviewReadiness, isWorkReadyForReview, type ReviewReadinessAssessment, } from "./tui/browser-gates.js"; const MORPH_VERSION = readMorphVersion(); function readMorphVersion(): string { try { const raw = fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"); const parsed = JSON.parse(raw) as { version?: string }; return parsed.version || "dev"; } catch { return "dev"; } } function buildWorkSpecMarkdown(plan: PlanOutput, spark?: SparkOutput): string { const waves = waveGroups(plan.tasks); const fileOverlaps = detectFileTargetOverlaps(plan.tasks); const lines: string[] = [ "# morph Pre-Work Specification Review", "", "Review this specification before WORK starts. Edit anything that needs clarification, scope adjustment, or constraints.", "The final text saved from this editor is passed to implementation and review agents as human-approved guidance.", "", ]; if (spark) { lines.push( "## Product Shape Contract", `- **Deliverable Type**: ${spark.productShape.deliverableType}`, `- **Runtime / Host**: ${spark.productShape.runtime}`, `- **Distribution**: ${spark.productShape.distribution}`, `- **Explicit User Intent**: ${spark.productShape.explicitUserIntent}`, "", "## Product Intent", spark.visionStatement, "", "### Core Features", ...spark.coreFeatures.map((feature) => `- ${feature}`), "", "### Success Criteria", ...(spark.successCriteria.length ? spark.successCriteria.map((item) => `- ${item}`) : ["- Not specified"]), "", "### Constraints", ...(spark.constraints.length ? spark.constraints.map((item) => `- ${item}`) : ["- None specified"]), "" ); } lines.push( "## Architecture", plan.architectureDiagram, "", "## Data Models", ...(plan.dataModels.length ? plan.dataModels.map((m) => `- ${m}`) : ["- None specified"]), "", "## Components", ...(plan.componentTree.length ? plan.componentTree.map((c) => `- ${c.name}: ${c.responsibility}${c.dependsOn.length ? ` (depends on: ${c.dependsOn.join(", ")})` : ""}`) : ["- None specified"]), "", "## Execution Waves" ); for (let i = 0; i < waves.length; i++) { lines.push("", `### Wave ${i + 1}`); for (const task of waves[i]) lines.push(`- [${task.id}] ${task.description}`, ` - Category: ${task.category}`, ` - Complexity: ${task.estimatedComplexity}`, ` - Depends on: ${task.dependsOn.length ? task.dependsOn.join(", ") : "none"}`, ` - Acceptance: ${task.acceptanceCriteria}`); } lines.push( "", "## File Target Overlaps", ...(fileOverlaps.length ? fileOverlaps.map((overlap) => `- [${overlap.severity.toUpperCase()}] ${overlap.file}: ${overlap.taskIds.join(", ")} (waves ${overlap.waveNumbers.join(", ")}) — ${overlap.suggestion}` ) : ["- None detected"]), "", "## QA Strategy", plan.qaStrategy, "", "## Risk Mitigations", ...(plan.riskMitigations.length ? plan.riskMitigations.map((r) => `- ${r}`) : ["- None specified"]), "", "## Human Adjustments / Approval Notes", plan.humanReviewNotes || "Approved as written." ); return lines.join("\n"); } function openFileInBrowser(filePath: string): void { const absolute = path.resolve(filePath); if (process.platform === "win32") spawn("cmd.exe", ["/c", "start", "", absolute], { detached: true, stdio: "ignore" }).unref(); else if (process.platform === "darwin") spawn("open", [absolute], { detached: true, stdio: "ignore" }).unref(); else spawn("xdg-open", [absolute], { detached: true, stdio: "ignore" }).unref(); } function hasProjectMarker(dir: string): boolean { return [ "package.json", "pyproject.toml", "requirements.txt", "Cargo.toml", "go.mod", "index.html", ].some((file) => fs.existsSync(path.join(dir, file))); } function inferProjectRoot(state: ReturnType, cwd: string): { absolutePath: string; relativePath: string; reason: string; } { const targetDirs = (state.planOutput?.tasks ?? []) .map((task) => task.targetDir) .filter((dir): dir is string => Boolean(dir)); const counts = new Map(); for (const dir of targetDirs) counts.set(dir, (counts.get(dir) ?? 0) + 1); const rankedTargets = [...counts.entries()] .sort((a, b) => b[1] - a[1]) .map(([dir]) => dir); for (const dir of rankedTargets) { const absolute = path.resolve(cwd, dir); if (fs.existsSync(absolute) && hasProjectMarker(absolute)) { return { absolutePath: absolute, relativePath: dir, reason: "Most planned work targeted this project directory.", }; } } const firstLevelDirs = fs.readdirSync(cwd, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")) .map((entry) => entry.name); for (const dir of firstLevelDirs) { const absolute = path.join(cwd, dir); if (hasProjectMarker(absolute)) { return { absolutePath: absolute, relativePath: dir, reason: "Detected a nested project directory with runnable project files.", }; } } return { absolutePath: cwd, relativePath: ".", reason: "Using the repository root.", }; } function detectQuickstart(projectRoot: string, repoRoot: string): string[] { const packageJsonPath = path.join(projectRoot, "package.json"); const relativeRoot = path.relative(repoRoot, projectRoot) || "."; const maybeCd = relativeRoot === "." ? [] : [`\`cd ${relativeRoot.replace(/\\/g, "/")}\``]; if (fs.existsSync(packageJsonPath)) { try { const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); const scripts = pkg.scripts ?? {}; const manager = fs.existsSync(path.join(projectRoot, "pnpm-lock.yaml")) ? "pnpm" : fs.existsSync(path.join(projectRoot, "yarn.lock")) ? "yarn" : "npm"; const install = manager === "yarn" ? "yarn install" : `${manager} install`; const runDev = scripts.dev ? manager === "npm" ? "npm run dev" : `${manager} dev` : scripts.start ? manager === "npm" ? "npm start" : `${manager} start` : undefined; const runTest = scripts.test ? manager === "npm" ? "npm test" : `${manager} test` : undefined; return [ ...maybeCd, `\`${install}\``, ...(runDev ? [`\`${runDev}\``] : []), ...(runTest ? [`\`${runTest}\` to verify the project`] : []), ]; } catch { // Fall through to generic guidance. } } if (fs.existsSync(path.join(projectRoot, "pyproject.toml")) || fs.existsSync(path.join(projectRoot, "requirements.txt"))) { return [ ...maybeCd, "`python -m venv .venv`", "Activate the virtual environment.", fs.existsSync(path.join(projectRoot, "requirements.txt")) ? "`pip install -r requirements.txt`" : "`pip install -e .`", ]; } if (fs.existsSync(path.join(projectRoot, "index.html"))) { return [ ...maybeCd, "Open `index.html` in a browser, or serve the directory with a simple static server.", ]; } return [ ...maybeCd, "Open the project directory.", "Review the changed files listed below.", "Use the repository README or stack-specific commands to run the project.", ]; } function buildFinalReportMarkdown(state: ReturnType, cwd: string): string { const spark = state.sparkOutput; const plan = state.planOutput; const review = state.reviewOutput; const ship = state.shipOutput; const changedFiles = [...new Set(state.workResults.flatMap((result) => result.filesChanged))]; const completedTasks = state.workResults.filter((result) => result.status === "done"); const projectRoot = inferProjectRoot(state, cwd); const quickstart = detectQuickstart(projectRoot.absolutePath, cwd); const lines: string[] = [ "# morph Final Handoff Report", "", ship ? `**Release**: v${ship.version} — ${ship.status}` : "**Release**: not recorded", "", "## Overview", spark?.visionStatement ?? "No product overview captured.", "", "## What was built", ...(spark?.coreFeatures.length ? spark.coreFeatures.map((feature) => `- ${feature}`) : ["- No feature summary captured."]), "", "## Quickstart", ...quickstart.map((step, index) => `${index + 1}. ${step}`), "", "## Primary deliverable", `- **Project root**: \`${projectRoot.relativePath}\``, `- **Why**: ${projectRoot.reason}`, "", "## Architecture", plan?.architectureDiagram ?? "No architecture diagram captured.", "", "## Main components", ...(plan?.componentTree.length ? plan.componentTree.map((component) => `- **${component.name}** — ${component.responsibility}`) : ["- No component breakdown captured."]), "", "## Delivered work", ...(completedTasks.length ? completedTasks.map((result) => `- **${result.taskId}** — ${result.summary}`) : ["- No completed work results recorded."]), "", "## Changed files", ...(changedFiles.length ? changedFiles.map((file) => `- \`${file}\``) : ["- No changed files recorded."]), "", "## Validation", review ? `Review status: **${review.status}** · efficiency score **${review.efficiencyScore}/10**` : "Review output not recorded.", ...(review?.testCoverageAssessment ? ["", review.testCoverageAssessment] : []), "", "## Release notes", ship?.changelog ?? "No changelog captured.", "", "## Deployment checklist", ...(ship?.deploymentChecklist.length ? ship.deploymentChecklist.map((item) => `- [${item.done ? "x" : " "}] ${item.item}`) : ["- No deployment checklist captured."]), "", "## Known limitations / follow-ups", ...(review?.requiredChanges.length ? review.requiredChanges.map((change) => `- ${change.severity}: ${change.description}`) : ["- No open review findings recorded."]), "", ...(ship?.rollbackPlan ? ["## Rollback plan", ship.rollbackPlan, ""] : []), ...(ship?.postReleaseNotes ? ["## Post-release monitoring", ship.postReleaseNotes, ""] : []), ]; return lines.join("\n"); } function buildFinalReportHtml(state: ReturnType, markdown: string, cwd: string): string { const spark = state.sparkOutput; const plan = state.planOutput; const review = state.reviewOutput; const ship = state.shipOutput; const projectRoot = inferProjectRoot(state, cwd); const quickstart = detectQuickstart(projectRoot.absolutePath, cwd); const quickstartHtml = quickstart .map((step) => escapeHtml(step).replace(/`([^`]+)`/g, "$1")); const changedFiles = [...new Set(state.workResults.flatMap((result) => result.filesChanged))]; const completedTasks = state.workResults.filter((result) => result.status === "done"); return ` morph Final Handoff Report
morph final handoff

${spark ? escapeHtml(spark.visionStatement.slice(0, 90)) : "Completed project"}

${ship ? `Release v${escapeHtml(ship.version)} · ${escapeHtml(ship.status)}` : "Release not recorded"}

${completedTasks.length} completed tasks
root ${escapeHtml(projectRoot.relativePath)}

Handoff summary

${escapeHtml(spark?.visionStatement ?? "No product overview captured.")}

Primary deliverable: ${escapeHtml(projectRoot.relativePath)} — ${escapeHtml(projectRoot.reason)}

What was built

${renderHtmlList(spark?.coreFeatures ?? [], "No feature summary captured.")}

Quickstart

    ${quickstartHtml.map((step) => `
  1. ${step}
  2. `).join("")}

Architecture

${escapeHtml(plan?.architectureDiagram ?? "No architecture diagram captured.")}

Main components

${renderHtmlList(plan?.componentTree.map((component) => `${component.name} — ${component.responsibility}`) ?? [], "No component breakdown captured.")}

Validation

${review ? `Review status: ${escapeHtml(review.status)} · efficiency score ${review.efficiencyScore}/10` : "Review output not recorded."}

${review?.testCoverageAssessment ? `

${escapeHtml(review.testCoverageAssessment)}

` : ""}

Delivered work

${renderHtmlList(completedTasks.map((result) => `${result.taskId} — ${result.summary}`), "No completed work results recorded.")}

Changed files

${renderHtmlList(changedFiles, "No changed files recorded.")}

Release notes

${escapeHtml(ship?.changelog ?? "No changelog captured.")}

Deployment checklist

${renderHtmlList(ship?.deploymentChecklist.map((item) => `${item.done ? "✓" : "○"} ${item.item}`) ?? [], "No deployment checklist captured.")}

Known limitations / follow-ups

${renderHtmlList(review?.requiredChanges.map((change) => `${change.severity}: ${change.description}`) ?? [], "No open review findings recorded.")}
${ship?.rollbackPlan ? `

Rollback plan

${escapeHtml(ship.rollbackPlan)}
` : ""} ${ship?.postReleaseNotes ? `

Post-release monitoring

${escapeHtml(ship.postReleaseNotes)}
` : ""}

Markdown snapshot

${escapeHtml(markdown)}
`; } export default function (pi: ExtensionAPI) { // ── Shared state ── let blackboard: Blackboard | null = null; let blackboardCwd: string | null = null; let activeWorkspaceCwd: string | null = null; let currentAbortController: AbortController | null = null; let currentTick = 0; let currentStatus: PipelineDisplay["status"] = "ready"; let currentHint: string | undefined; let animationInterval: NodeJS.Timeout | null = null; let requestAnimationRender: (() => void) | null = null; let webServer: Server | null = null; const subagentActivities = new Map(); const activeFileActivities = new Map(); const recentFileActivities: FileActivity[] = []; function setCurrentStatus(status: PipelineDisplay["status"], hint?: string): void { currentStatus = status; currentHint = hint; } async function waitConfirm(ctx: any, title: string, desc: string, phase: string): Promise { const previousStatus = currentStatus; const previousHint = currentHint; currentStatus = "waiting"; currentHint = "approval needed | browser or Pi"; updateWidget(ctx); return new Promise((resolve) => { let resolved = false; // Web UI approval listener const onApprove = (p: string) => { if (p === phase && !resolved) { resolved = true; serverEvents.removeListener("approve", onApprove); ctx.ui.notify(`Approved via Web UI`, "success" as any); currentStatus = previousStatus; currentHint = previousHint; updateWidget(ctx); resolve(true); } }; serverEvents.on("approve", onApprove); // Terminal confirmation ctx.ui.confirm(title, desc).then((proceed: boolean) => { if (!resolved) { resolved = true; serverEvents.removeListener("approve", onApprove); currentStatus = previousStatus; currentHint = previousHint; updateWidget(ctx); resolve(proceed); } }); }); } function getBB(cwd?: string): Blackboard { const workspaceCwd = path.resolve(cwd || activeWorkspaceCwd || process.cwd()); if (!blackboard || blackboardCwd !== workspaceCwd) { blackboard = new Blackboard(workspaceCwd); blackboardCwd = workspaceCwd; } return blackboard; } function resetBB(): void { blackboard = null; blackboardCwd = null; currentAbortController?.abort(); currentAbortController = null; activeFileActivities.clear(); recentFileActivities.length = 0; } function bindWorkspace(cwd: string): Blackboard { activeWorkspaceCwd = path.resolve(cwd); return getBB(activeWorkspaceCwd); } function deletePipelineState(cwd: string): void { currentAbortController?.abort(); currentAbortController = null; blackboard = null; blackboardCwd = null; activeFileActivities.clear(); recentFileActivities.length = 0; fs.rmSync(getMorphDir(cwd), { recursive: true, force: true }); } function clearStaleEnvironmentFailures(bb: Blackboard): number { const staleEnvironmentFailures = bb .getState() .workResults .filter( (result) => result.status === "failed" && ["TOOL_FAILURE", "AUTH_OR_QUOTA_FAILURE", "CLI_LAUNCH_FAILURE"].includes(result.failureKind || "") ) .map((result) => result.taskId); for (const taskId of staleEnvironmentFailures) { clearWorkResultBranch(bb, taskId); } return staleEnvironmentFailures.length; } function syncInheritedModelConfig(ctx: any): { changed: boolean; clearedFailures: number } { const bb = bindWorkspace(ctx.cwd); const state = bb.getState(); if (state.config?.source === "manual") { return { changed: false, clearedFailures: 0 }; } const model = ctx.model; if (!model?.provider && !model?.id) { return { changed: false, clearedFailures: 0 }; } const nextProvider = model.provider ? String(model.provider) : undefined; const nextModel = model.id ? String(model.id) : undefined; const changed = state.config?.provider !== nextProvider || state.config?.model !== nextModel || state.config?.source !== "inherited"; if (!changed) { return { changed: false, clearedFailures: 0 }; } bb.setConfig({ provider: nextProvider, model: nextModel, source: "inherited", }); return { changed: true, clearedFailures: clearStaleEnvironmentFailures(bb), }; } function ensureWebServer(): void { if (!webServer) { webServer = startMorphServer(getBB(), 4040); } } function createBrowserDecisionWaiter(phase: string): { promise: Promise<"approve" | "reject">; dispose: () => void; } { let dispose = () => {}; const promise = new Promise<"approve" | "reject">((resolve) => { dispose = () => { serverEvents.removeListener("approve", onApprove); serverEvents.removeListener("reject", onReject); }; const onApprove = (p: string) => { if (p === phase) { dispose(); resolve("approve"); } }; const onReject = (p: string) => { if (p === phase) { dispose(); resolve("reject"); } }; serverEvents.on("approve", onApprove); serverEvents.on("reject", onReject); }); return { promise, dispose }; } async function runBrowserApprovalGate(ctx: any, options: { phase: string; htmlPath: string; browserMessage: string; confirmTitle: string; confirmDescription: string; hint: string; }): Promise { const previousStatus = currentStatus; const previousHint = currentHint; currentStatus = "waiting"; currentHint = options.hint; updateWidget(ctx); ensureWebServer(); openFileInBrowser(options.htmlPath); pi.sendMessage({ customType: "morph", content: options.browserMessage, display: true, details: { phase: options.phase, htmlPath: options.htmlPath }, }); const browserDecision = createBrowserDecisionWaiter(options.phase); const decision = await Promise.race([ browserDecision.promise, ctx.ui.confirm(options.confirmTitle, options.confirmDescription) .then((approved: boolean) => approved ? "approve" as const : "reject" as const), ]); browserDecision.dispose(); currentStatus = previousStatus; currentHint = previousHint; updateWidget(ctx); return decision === "approve"; } function writeFinalReportArtifacts(ctx: any): { markdownPath: string; htmlPath: string } { const morphDir = getMorphDir(ctx.cwd); fs.mkdirSync(morphDir, { recursive: true }); const state = bindWorkspace(ctx.cwd).getState(); const markdown = buildFinalReportMarkdown(state, ctx.cwd); const html = buildFinalReportHtml(state, markdown, ctx.cwd); const markdownPath = path.join(morphDir, "final-report.md"); const htmlPath = path.join(morphDir, "final-report.html"); fs.writeFileSync(markdownPath, markdown, "utf-8"); fs.writeFileSync(htmlPath, html, "utf-8"); return { markdownPath, htmlPath }; } async function reviewWorkSpecGate(ctx: any, planOutput: PlanOutput): Promise { const bb = bindWorkspace(ctx.cwd); const morphDir = getMorphDir(ctx.cwd); fs.mkdirSync(morphDir, { recursive: true }); const sparkOutput = bb.getState().sparkOutput; const draftMarkdown = buildWorkSpecMarkdown(planOutput, sparkOutput); const markdownPath = path.join(morphDir, "work-spec.md"); const htmlPath = path.join(morphDir, "work-approval.html"); const edited = await ctx.ui.editor("Review/edit WORK specification before implementation", draftMarkdown); if (edited === undefined) { ctx.ui.notify("WORK paused. Re-run /morph:run or /morph:work when ready.", "info"); setCurrentStatus("ready"); currentHint = "work paused | /morph:run"; updateWidget(ctx); return false; } fs.writeFileSync(markdownPath, edited, "utf-8"); planOutput.humanReviewNotes = edited; bb.setPlanOutput(planOutput); fs.writeFileSync(htmlPath, buildWorkSpecHtml(planOutput, edited, sparkOutput), "utf-8"); const approved = await runBrowserApprovalGate(ctx, { phase: "pre-work", htmlPath, browserMessage: `# Pre-Work Specification Review Opened \`${htmlPath}\` for the implementation approval gate. Review the decision summary there, then approve or pause from the browser or pi prompt.`, confirmTitle: "Approve WORK specification?", confirmDescription: "The approval gate is open in your browser. Continue with implementation?", hint: "approve work spec | browser or Pi", }); if (!approved) { bb.recordDecision("plan", "Human paused pre-work specification", `Approval artifact: ${htmlPath}`); ctx.ui.notify("WORK paused before implementation. Re-run /morph:run or /morph:work when ready.", "info"); return false; } bb.recordDecision("plan", "Human approved pre-work specification", `Approval artifact: ${htmlPath}`); ctx.ui.notify("Pre-work specification approved. Starting WORK...", "success" as any); return true; } async function reviewSparkGate(ctx: any, sparkOutput: SparkOutput): Promise { const bb = bindWorkspace(ctx.cwd); const morphDir = getMorphDir(ctx.cwd); fs.mkdirSync(morphDir, { recursive: true }); const htmlPath = path.join(morphDir, "spark-approval.html"); fs.writeFileSync(htmlPath, buildSparkApprovalHtml(sparkOutput), "utf-8"); const approved = await runBrowserApprovalGate(ctx, { phase: "spark", htmlPath, browserMessage: `# Spark Direction Review Opened \`${htmlPath}\` for the product-direction approval gate. Review the brief at a glance, then approve or pause from the browser or pi prompt.`, confirmTitle: "Proceed to Plan phase?", confirmDescription: "The Spark approval gate is open in your browser. Continue into planning?", hint: "approve product direction | browser or Pi", }); bb.recordDecision("spark", approved ? "Human approved product direction" : "Human paused product direction", `Approval artifact: ${htmlPath}`); return approved; } async function reviewShipGate(ctx: any): Promise { const bb = bindWorkspace(ctx.cwd); const state = bb.getState(); if (!state.reviewOutput) return false; const morphDir = getMorphDir(ctx.cwd); fs.mkdirSync(morphDir, { recursive: true }); const htmlPath = path.join(morphDir, "ship-approval.html"); fs.writeFileSync(htmlPath, buildReviewApprovalHtml(state), "utf-8"); const approved = await runBrowserApprovalGate(ctx, { phase: "review", htmlPath, browserMessage: `# Release Readiness Review Opened \`${htmlPath}\` for the review-to-ship approval gate. Inspect the verdict, flags, and release-readiness summary there before continuing.`, confirmTitle: "Proceed to Ship phase?", confirmDescription: "The release-readiness gate is open in your browser. Continue into SHIP?", hint: "approve release readiness | browser or Pi", }); bb.recordDecision("review", approved ? "Human approved release readiness" : "Human paused release readiness", `Approval artifact: ${htmlPath}`); return approved; } async function reviewExceptionGate(ctx: any, readiness: ReviewReadinessAssessment): Promise { const bb = bindWorkspace(ctx.cwd); const state = bb.getState(); if (!state.reviewOutput) return false; const morphDir = getMorphDir(ctx.cwd); fs.mkdirSync(morphDir, { recursive: true }); const htmlPath = path.join(morphDir, "ship-exception.html"); fs.writeFileSync(htmlPath, buildReviewExceptionHtml(state, readiness), "utf-8"); const accepted = await runBrowserApprovalGate(ctx, { phase: "review-exception", htmlPath, browserMessage: `# Release Exception Review Opened \`${htmlPath}\` because the normal Review → Ship gate was withheld. Morph could not clear the ship-readiness floor or repair the issue automatically, so continuing now requires an explicit human exception.`, confirmTitle: "Accept release exception?", confirmDescription: "The normal release gate was withheld. Accept this exception and continue into SHIP anyway?", hint: "resolve release exception | browser or Pi", }); bb.recordDecision("review", accepted ? "Human accepted release exception" : "Human paused release exception", `Approval artifact: ${htmlPath}`); return accepted; } // ── Build display state from blackboard ── function buildPipelineDisplay(taskOverride?: TaskDisplay[]): PipelineDisplay { const bb = getBB(); const state = bb.getState(); const tasks: TaskDisplay[] = []; const agents: AgentActivity[] = []; // Build task list from plan + work results if (state.planOutput) { const entries = state.workResults.map((r) => [r.taskId, r] as const); const resultMap = new Map(entries); for (const task of state.planOutput.tasks) { const result = resultMap.get(task.id); tasks.push({ id: task.id, description: task.description, status: result ? result.status === "done" ? "done" : result.status === "blocked" ? "blocked" : "failed" : "pending", }); } } // Build agent activity from current phase const phaseAgents = getPhaseAgents(state.phase); for (const a of phaseAgents) { agents.push({ name: a.name, role: a.role, phase: state.phase, status: "idle", }); } const liveSubs = [...subagentActivities.values()].filter( (activity) => activity.status === "running" || activity.status === "idle" ); const hasLiveAgent = liveSubs.length > 0 || state.activeAgents.length > 0; const visibleTasks: TaskDisplay[] = (taskOverride ?? tasks).map((task) => !hasLiveAgent && task.status === "running" ? { ...task, status: "pending" as const } : task ); const visibleActiveFiles = hasLiveAgent ? [...activeFileActivities.values()] : [...activeFileActivities.values()].map((activity) => ({ ...activity, status: "done" as const })); const fileActivities = [...visibleActiveFiles, ...recentFileActivities].slice(0, 6); const fileCollisions = buildFileCollisions([...activeFileActivities.values()]); return { phase: state.phase, status: currentStatus, tasks: visibleTasks, agents, tokenLedger: state.tokenLedger, tick: currentTick, startedAt: state.startedAt, versionLabel: MORPH_VERSION, runtimeLabel: `node ${process.version.replace(/^v/, "")}`, subagentActivities: liveSubs.length > 0 ? liveSubs : undefined, fileActivities, fileCollisions, phaseContext: buildPhaseContext(state, visibleTasks, liveSubs), footerHint: currentHint, restoredCheckpointCount: Object.keys(state.flowCheckpoints[state.phase] || {}).length, }; } function getPhaseAgents(phase: string) { switch (phase) { case "spark": return SPARK_AGENTS; case "plan": return PLAN_AGENTS; case "work": return WORK_AGENTS; case "review": return REVIEW_AGENTS; case "ship": return SHIP_AGENTS; default: return []; } } function clearSubagentActivity(name: string): void { subagentActivities.delete(name); } function pushRecentFileActivity(activity: FileActivity): void { recentFileActivities.unshift(activity); recentFileActivities.splice(5); } function setTaskFileActivities(taskId: string, activities: FileActivity[]): void { for (const key of [...activeFileActivities.keys()]) { if (key.startsWith(`${taskId}:`)) activeFileActivities.delete(key); } for (const activity of activities) { activeFileActivities.set(`${taskId}:${activity.path}`, activity); } } function completeTaskFileActivities(taskId: string): void { for (const [key, activity] of [...activeFileActivities.entries()]) { if (!key.startsWith(`${taskId}:`)) continue; activeFileActivities.delete(key); pushRecentFileActivity({ ...activity, status: "done" }); } } function buildFileCollisions(activities: FileActivity[]): FileCollision[] { const taskIdsByPath = new Map>(); for (const activity of activities) { if (activity.status !== "active") continue; const taskIds = taskIdsByPath.get(activity.path) ?? new Set(); taskIds.add(activity.taskId); taskIdsByPath.set(activity.path, taskIds); } return [...taskIdsByPath.entries()] .filter(([, taskIds]) => taskIds.size > 1) .map(([path, taskIds]) => ({ path, taskIds: [...taskIds].sort(), unexpectedTaskIds: [...taskIds] .filter((taskId) => !isExpectedTaskFile(taskId, path)) .sort(), })) .sort((a, b) => a.path.localeCompare(b.path)); } function isExpectedTaskFile(taskId: string, filePath: string): boolean { const task = getBB().getState().planOutput?.tasks.find((candidate) => candidate.id === taskId); if (!task) return false; return (task.files ?? []).some((pattern) => matchesTaskFilePattern(filePath, pattern)); } function matchesTaskFilePattern(filePath: string, pattern: string): boolean { const normalizedFile = filePath.replace(/\\/g, "/"); const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\.\//, ""); if (normalizedPattern.endsWith("/**")) { const prefix = normalizedPattern.slice(0, -3).replace(/\/$/, ""); return normalizedFile === prefix || normalizedFile.startsWith(`${prefix}/`); } return normalizedFile === normalizedPattern; } function buildPhaseContext( state: ReturnType, tasks: TaskDisplay[], liveSubs: SubagentActivity[] ): PhaseContext | undefined { const activeAgents = liveSubs.length; const liveNames = new Set(liveSubs.map((agent) => agent.name)); const checkpoints = state.flowCheckpoints[state.phase] || {}; const checkpointKeys = new Set(Object.keys(checkpoints)); const restoredMark = "↺"; const agentCheckpointKey: Record = { visionary: state.phase === "spark" ? "visionary" : "", critic: state.phase === "spark" ? "critic" : "", architect: state.phase === "plan" ? "architect" : "", "qa-expert": state.phase === "plan" ? "qa" : "", "efficiency-mgr": state.phase === "plan" ? "efficiency" : "", "qa-auditor": state.phase === "review" ? "qa" : "", "perf-guru": state.phase === "review" ? "perf" : "", "end-user": state.phase === "review" ? "user" : "", "tech-lead": state.phase === "review" ? "techLead" : "", "devops-sre": state.phase === "ship" ? "devops" : "", "release-consultant": state.phase === "ship" ? "consultant" : "", }; const agentMark = (name: string) => { const checkpoint = agentCheckpointKey[name]; if (checkpoint && checkpointKeys.has(checkpoint)) return restoredMark; return liveNames.has(name) ? "●" : "○"; }; const compactTask = (task: TaskDisplay) => `${task.status === "done" ? "✓" : task.status === "running" ? "●" : task.status === "failed" || task.status === "blocked" ? "!" : "○"} ${task.id}`; const runningTasks = tasks.filter((task) => task.status === "running"); const pendingTasks = tasks.filter((task) => task.status === "pending"); const doneTasks = tasks.filter((task) => task.status === "done"); const failedTasks = tasks.filter((task) => task.status === "failed"); const blockedTasks = tasks.filter((task) => task.status === "blocked"); switch (state.phase) { case "spark": return { title: "SPARK BOARD", lines: [ `Visionary ${agentMark("visionary")} -> Critic ${agentMark("critic")}`, checkpointKeys.size > 0 ? `restored ${[...checkpointKeys].join(", ")}` : "restored —", state.sparkOutput ? `features ${state.sparkOutput.coreFeatures.length} · risks ${state.sparkOutput.risks.length}` : "shaping PRD · pressure-testing idea", state.sparkOutput?.technicalStackRecommendation ? `stack -> ${state.sparkOutput.technicalStackRecommendation}` : "next -> synthesize final PRD", ]}; case "plan": return buildPlanIntelligenceContext(state); case "work": { const workIsBusy = currentStatus === "busy"; const shownRunning = runningTasks.slice(0, 3).map(compactTask).join(" "); const shownPending = pendingTasks.slice(0, 3).map(compactTask).join(" "); const compactQueue = tasks.slice(0, 5).map((task) => task.status === "done" ? "✓" : task.status === "running" ? "●" : task.status === "failed" || task.status === "blocked" ? "!" : "○" ).join(" "); const lastFailure = [...state.workResults].reverse().find((result) => result.status !== "done"); const halted = (failedTasks.length > 0 || blockedTasks.length > 0) && runningTasks.length === 0; if (halted) { return { title: "WORK HALTED", lines: [ `done ${doneTasks.length}/${tasks.length} · failed ${failedTasks.length} · blocked ${blockedTasks.length}`, `pending ${pendingTasks.length} untouched`, lastFailure ? `task ${lastFailure.taskId} ${lastFailure.status}` : "task failure recorded", lastFailure ? `files ${lastFailure.filesChanged.length} changed` : "files —", lastFailure ? lastFailure.summary.replace(/\s+/g, " ").slice(0, 72) : "inspect task history", "next -> recover or inspect status", ]}; } if (!workIsBusy && runningTasks.length === 0 && pendingTasks.length > 0 && activeAgents === 0) { return { title: "WORK CONTROL", lines: [ `done ${doneTasks.length}/${tasks.length} · failed ${failedTasks.length} · blocked ${blockedTasks.length}`, compactQueue ? `queue ${compactQueue}` : "queue —", shownPending ? `next ${shownPending}` : "next work pending", "lanes no active agent", "next -> resume or recover", ]}; } return { title: "WORK CONTROL", lines: [ `done ${doneTasks.length}/${tasks.length} · failed ${failedTasks.length} · blocked ${blockedTasks.length}`, compactQueue ? `queue ${compactQueue}` : "queue —", shownRunning ? `doing ${shownRunning}` : "doing —", shownPending ? `next ${shownPending}` : "next review gate", activeAgents > 0 ? `lanes engineer ${agentMark("engineer")} -> reviewer ${agentMark("peer-reviewer")}` : "next -> review when queue clears", ]}; } case "review": { return buildReviewIntelligenceContext(state); } case "ship": return { title: "SHIP BOARD", lines: [ `DevOps ${agentMark("devops-sre")} -> Release ${agentMark("release-consultant")}`, checkpointKeys.size > 0 ? `restored ${[...checkpointKeys].join(", ")}` : "restored —", state.shipOutput ? `${state.shipOutput.status} · v${state.shipOutput.version}` : "preparing release package", state.reviewOutput ? `review -> ${state.reviewOutput.status}` : "review -> pending", ]}; default: return undefined; } } function buildPlanIntelligenceContext( state: ReturnType ): PhaseContext { const telemetry = state.planTelemetry; const shape = state.sparkOutput?.productShape; const target = shape ? `${shape.deliverableType} · ${shape.runtime} · ${shape.distribution}` : "product shape not captured"; const lines = [`Target ${target}`]; if (telemetry?.architect.tasksDrafted || telemetry?.architect.componentsMapped) { const parts = [ telemetry.architect.tasksDrafted !== undefined ? `${telemetry.architect.tasksDrafted} tasks drafted` : undefined, telemetry.architect.componentsMapped !== undefined ? `${telemetry.architect.componentsMapped} components mapped` : undefined, ].filter(Boolean); lines.push(`Architect ${parts.join(" · ")}`); } else { lines.push("Architect drafting architecture"); } lines.push( telemetry?.qa.issuesFound !== undefined ? `QA ${telemetry.qa.issuesFound} review signals${telemetry.qa.notableGap ? ` · ${telemetry.qa.notableGap}` : ""}` : "QA waiting for task graph" ); lines.push( telemetry?.efficiency.observationsFound !== undefined ? `Efficiency ${telemetry.efficiency.observationsFound} optimization signals${telemetry.efficiency.notableChange ? ` · ${telemetry.efficiency.notableChange}` : ""}` : "Efficiency waiting for task graph" ); if (telemetry?.recovery) { lines.push(`Recovery ${telemetry.recovery.issue}`); lines.push(`Next ${telemetry.recovery.action}`); } else if (telemetry?.finalPlan.tasks !== undefined) { lines.push( `Final plan ${telemetry.finalPlan.tasks} tasks · ${telemetry.finalPlan.waves ?? "?"} waves · ${telemetry.finalPlan.estimatedEffort ?? "effort ?"}` ); if (telemetry.watchlist.length) { lines.push(`Watchlist ${telemetry.watchlist[0]}`); } if (telemetry.fileOverlaps.length) { const first = telemetry.fileOverlaps[0]; lines.push(`Overlap ${first.severity.toUpperCase()} ${first.taskIds.join(" + ")} -> ${first.file}`); } if (telemetry.fileOverlapRepairs.length) { const firstRepair = telemetry.fileOverlapRepairs[0]; lines.push(`Serialized ${firstRepair.serializedTaskIds.join(" -> ")} -> ${firstRepair.file}`); } lines.push(`Next ${telemetry.nextStep}`); } else { lines.push(`Next ${telemetry?.nextStep ?? "draft first plan"}`); } return { title: "PLAN INTELLIGENCE", lines }; } function buildReviewIntelligenceContext( state: ReturnType ): PhaseContext { const telemetry = state.reviewTelemetry; const review = state.reviewOutput; const severities: Record = { critical: 0, major: 0, minor: 0, "nice-to-have": 0 }; for (const change of review?.requiredChanges ?? []) severities[change.severity]++; const route = (used?: boolean) => used ? "used" : "skipped"; const lines = telemetry ? [ `Routing QA ${route(telemetry.routing.qa)} | Perf ${route(telemetry.routing.perf)} | User ${route(telemetry.routing.user)}`, ] : ["Routing preparing specialist routing"]; if (telemetry?.qa.signalsFound !== undefined) { lines.push( telemetry.routing.qa ? `QA ${telemetry.qa.signalsFound} review signals${telemetry.qa.notableGap ? ` | ${telemetry.qa.notableGap}` : ""}` : "QA skipped" ); } else { lines.push(telemetry?.routing.qa ? "QA specialist running" : "QA waiting for routing"); } if (telemetry?.perf.signalsFound !== undefined) { lines.push( telemetry.routing.perf ? `Performance ${telemetry.perf.signalsFound} review signals${telemetry.perf.notableConcern ? ` | ${telemetry.perf.notableConcern}` : ""}` : "Performance skipped" ); } else { lines.push(telemetry?.routing.perf ? "Performance specialist running" : "Performance waiting for routing"); } if (telemetry?.user.signalsFound !== undefined) { lines.push( telemetry.routing.user ? `User ${telemetry.user.signalsFound} review signals${telemetry.user.notableConcern ? ` | ${telemetry.user.notableConcern}` : ""}` : "User skipped" ); } else { lines.push(telemetry?.routing.user ? "User specialist running" : "User waiting for routing"); } if (telemetry?.recovery) { lines.push(`Recovery ${telemetry.recovery.issue}`); lines.push(`Next ${telemetry.recovery.action}`); } else if (review || telemetry?.synthesis.verdict) { const verdict = telemetry?.synthesis.verdict ?? review?.status ?? "pending"; const score = telemetry?.synthesis.score ?? review?.efficiencyScore; const requiredChanges = telemetry?.synthesis.requiredChanges ?? review?.requiredChanges.length ?? 0; const securityIssues = telemetry?.synthesis.securityIssues ?? review?.securityIssues.length ?? 0; lines.push(`Verdict ${verdict}${score !== undefined ? ` | score ${score}/10` : ""}`); lines.push( `Findings ${severities.critical} critical | ${severities.major} major | ${severities.minor} minor | ${requiredChanges} required` ); lines.push(`Security ${securityIssues} issue${securityIssues === 1 ? "" : "s"}`); if (telemetry?.synthesis.coverageAssessment) { lines.push(`Coverage ${telemetry.synthesis.coverageAssessment}`); } lines.push(`Next ${telemetry?.nextStep ?? "await review decision"}`); } else { const synthesis = telemetry?.stage === "synthesizing" ? "tech lead consolidating findings" : telemetry?.stage === "specialist-review" ? "waiting on specialists" : "routing specialists"; lines.push(`Synthesis ${synthesis}`); lines.push(`Next ${telemetry?.nextStep ?? "route review specialists"}`); } return { title: "REVIEW INTELLIGENCE", lines }; } function updateSubagentEvent(agentName: string, role: string, taskId: string, event: any): void { let entry = subagentActivities.get(agentName); if (!entry) { entry = { name: agentName, role, taskId, status: "running", currentTool: "", lastAction: "", turns: 0, toolDetail: "", lastEventAt: Date.now() }; subagentActivities.set(agentName, entry); } entry.taskId = taskId; entry.status = "running"; entry.lastEventAt = Date.now(); switch (event.type) { case "message_start": entry.currentTool = ""; entry.lastAction = "responding..."; break; case "text": if (event.text?.trim()) entry.lastAction = event.text.trim(); break; case "tool_use": { entry.currentTool = String(event.name || "").toLowerCase(); const args = event.input || event.args || {}; entry.toolDetail = typeof args === "object" ? args.path || args.filePath || args.file_path || args.command || args.pattern || args.query || "" : String(args).slice(0, 40); break; } case "tool_result": case "tool_result_end": { if (event.content?.[0]?.text) entry.lastAction = event.content[0].text.slice(0, 60).replace(/\n/g, " "); break; } case "message_end": entry.turns++; entry.currentTool = ""; entry.lastAction = `turn ${entry.turns} complete`; break; } } // ── Update the phase widget with colored pipeline progress ── function updateWidget(ctx: { ui: { setWidget: (id: string, lines: string[] | ((tui: any, theme: any) => { render: (w: number) => string[]; invalidate: () => void }), opts?: any) => void; }; }) { setPipelineWidget(ctx as any, () => buildPipelineDisplay()); } function setPipelineWidget( ctx: { ui: { setWidget: (id: string, lines: string[] | ((tui: any, theme: any) => { render: (w: number) => string[]; invalidate: () => void }), opts?: any) => void; }; }, getDisplay: () => PipelineDisplay ) { ctx.ui.setWidget( "morph", (tui: any, theme: any) => { requestAnimationRender = () => tui.requestRender(); return buildPipelineProgressWidget(getDisplay, theme); }, { placement: "aboveEditor" } ); } // ── Session lifecycle ── pi.on("session_start", async (_event, ctx) => { const bb = bindWorkspace(ctx.cwd); let state = bb.getState(); if (state.activeAgents.length > 0) { bb.clearActiveAgents(); state = bb.getState(); } // Follow the parent session's active model unless the user explicitly pins // Morph to a manual provider/model via /morph:config. const modelSync = syncInheritedModelConfig(ctx as any); if (modelSync.changed && modelSync.clearedFailures > 0) { const conf = bb.getState().config; ctx.ui.notify( `Morph now follows the parent session model: provider=${conf?.provider || "default"}, model=${conf?.model || "default"}. Cleared ${modelSync.clearedFailures} stale environment-failure result${modelSync.clearedFailures === 1 ? "" : "s"} from the previous model.`, "info" ); } state = bb.getState(); const regressionRecovery = restoreTrustedRegressionSnapshot(bb, state); if (regressionRecovery.restored) { state = regressionRecovery.state; ctx.ui.notify( `State regression detected; restored the last safe work snapshot (${regressionRecovery.restoredTaskCount} completed task${regressionRecovery.restoredTaskCount === 1 ? "" : "s"} recovered).`, "warning" ); } let recovery = summarizeRecoveryState(state); const archivedRecovery = recovery.kind === "stale" || state.phase === "idle" ? bb.getRecoverableArchives()[0] : undefined; if (archivedRecovery) { const archived = summarizeRecoveryState(archivedRecovery.state); const restoreArchive = await ctx.ui.confirm( "Restore archived morph flow?", `The active state is empty, but Morph found a newer recoverable ${archivedRecovery.state.phase.toUpperCase()} snapshot from ${archivedRecovery.modifiedAt.toLocaleString()} with ${archived.totalTasks > 0 ? `${archived.doneTasks}/${archived.totalTasks} tasks done` : "saved progress"}. Restore it now?` ); if (restoreArchive) { bb.restoreArchivedState(archivedRecovery); state = bb.getState(); recovery = summarizeRecoveryState(state); ctx.ui.notify(`Restored archived ${state.phase} flow from history.`, "info"); } } const emitRecoverableNotice = () => { ctx.ui.notify( `${recovery.message} -- /morph:recover to resume`, recovery.failedTasks > 0 || recovery.blockedTasks > 0 ? "warning" : "info" ); pi.sendMessage({ customType: "morph", content: [ "# Recovery detected", "", `Morph found an unfinished flow in this folder.`, "", `- **Phase**: ${state.phase}`, `- **Tasks**: ${recovery.totalTasks > 0 ? `${recovery.doneTasks}/${recovery.totalTasks} done` : "not planned yet"}`, `- **Failed**: ${recovery.failedTasks}`, `- **Blocked**: ${recovery.blockedTasks}`, `- **Checkpoints**: ${recovery.checkpointCount}`, "", recovery.checkpointCount > 0 ? "**Next**: choose Resume now to continue from the last safe checkpoint, or decline to inspect first." : "**Next**: choose Resume now to continue from the current phase, or decline to inspect first.", ].join("\n"), display: true, details: { phase: "recovery-detected" }, }); }; const emitRestartableNotice = () => { ctx.ui.notify(`${recovery.message} -- /morph:run to continue`, "info"); pi.sendMessage({ customType: "morph", content: [ "# Unfinished setup found", "", `Morph found an unfinished ${state.phase.toUpperCase()} setup in this folder, but no saved checkpoint yet.`, "", `- **Phase**: ${state.phase}`, `- **Checkpoints**: ${recovery.checkpointCount}`, "", "**Next**: choose Continue now to restart the current phase from saved context, or decline to inspect first.", ].join("\n"), display: true, details: { phase: "restartable-detected" }, }); }; if (recovery.kind === "stale") { bb.transition("idle"); state = bb.getState(); recovery = summarizeRecoveryState(state); ctx.ui.notify("morph cleared an empty stale flow marker in this folder. Ready for a new run.", "info"); } else if (state.phase === "done") { ctx.ui.notify("morph — completed flow found in this folder. /morph:status for details or /morph:reset to start over.", "info"); } else { ctx.ui.notify("morph — Type /morph:run to start your project", "info"); } // Start animation loop if (!animationInterval) { animationInterval = setInterval(() => { currentTick++; requestAnimationRender?.(); }, 100); } // Show persistent widget and status bar updateWidget(ctx as any); ctx.ui.setStatus( "morph", recovery.kind === "recoverable" ? `morph: RECOVERY ${state.phase.toUpperCase()} -- /morph:recover` : recovery.kind === "restartable" ? `morph: CONTINUE ${state.phase.toUpperCase()} -- /morph:run` : state.phase === "done" ? "morph: DONE -- /morph:status" : "morph: READY -- Type /morph:run " ); if (recovery.kind === "recoverable") { const resumeNow = await ctx.ui.confirm( "Resume unfinished morph flow?", recovery.checkpointCount > 0 ? `${state.phase.toUpperCase()} has ${recovery.checkpointCount} saved checkpoint${recovery.checkpointCount === 1 ? "" : "s"}. Resume from the last safe point now?` : `${state.phase.toUpperCase()} is unfinished. Continue from the current phase now?` ); if (resumeNow) { const diagnosis = diagnoseRecoveryState(state); state = prepareExplicitWorkRecovery(bb, state, diagnosis); ctx.ui.notify("Recovered unfinished morph flow; resuming now...", "info"); await runMorphPipeline("", ctx as any); } else { emitRecoverableNotice(); ctx.ui.notify("Recovery paused. Use /morph:recover when you are ready.", "info"); } } else if (recovery.kind === "restartable") { const continueNow = await ctx.ui.confirm( "Continue unfinished morph setup?", `${state.phase.toUpperCase()} has saved context but no checkpoint yet. Restart the current phase now?` ); if (continueNow) { ctx.ui.notify("Continuing unfinished morph setup...", "info"); await runMorphPipeline("", ctx as any); } else { emitRestartableNotice(); ctx.ui.notify("Continuation paused. Use /morph:run when you are ready.", "info"); } } }); pi.on("session_shutdown", async () => { if (animationInterval) clearInterval(animationInterval); animationInterval = null; requestAnimationRender = null; currentAbortController?.abort(); resetBB(); }); // ── Keyboard shortcuts ── pi.registerShortcut("ctrl+m s" as any, { description: "morph: Spark (refine idea)", handler: async (ctx) => { const text = ctx.ui.getEditorText?.() || ""; if (text.trim()) { ctx.ui.setEditorText?.(`/morph:run ${text}`); } else { ctx.ui.notify("Type your idea first, then Ctrl+M S", "info"); } }, }); pi.registerShortcut("ctrl+m p" as any, { description: "morph: Plan", handler: async (ctx) => { ctx.ui.setEditorText?.("/morph:plan"); }, }); pi.registerShortcut("ctrl+m w" as any, { description: "morph: Work", handler: async (ctx) => { ctx.ui.setEditorText?.("/morph:work"); }, }); pi.registerShortcut("ctrl+m r" as any, { description: "morph: Review", handler: async (ctx) => { ctx.ui.setEditorText?.("/morph:review"); }, }); pi.registerShortcut("ctrl+m h" as any, { description: "morph: Ship", handler: async (ctx) => { ctx.ui.setEditorText?.("/morph:ship"); }, }); pi.registerShortcut("ctrl+m g" as any, { description: "morph: Guided run (full pipeline)", handler: async (ctx) => { const text = ctx.ui.getEditorText?.() || ""; if (text.trim()) { ctx.ui.setEditorText?.("/morph:run " + text); } else { ctx.ui.setEditorText?.("/morph:run"); } }, }); // ═══════════════════════════════════════════ // PLAN // ═══════════════════════════════════════════ pi.registerCommand("morph:plan", { description: "Plan: create architecture plan from PRD (Architect + QA + Efficiency)", handler: async (_args, ctx) => { const bb = bindWorkspace(ctx.cwd); const state = bb.getState(); if (!state.sparkOutput) { ctx.ui.notify("No spark output. Run /morph:run first.", "error"); return; } bb.transition("plan"); setCurrentStatus("busy"); updateWidget(ctx as any); ctx.ui.setStatus("morph", "morph:plan Architect designing..."); ctx.ui.notify("Plan: Architect + QA + Efficiency Manager working...", "info"); try { currentAbortController = new AbortController(); // Stage 1: Architect ctx.ui.setStatus("morph", "morph:plan Architect -> architecture + tasks"); updateWidget(ctx as any); // Stage 2: QA + Efficiency (parallel) — status updates ctx.ui.setStatus("morph", "morph:plan QA + Efficiency reviewing..."); const planOutput = await executePlanFlow({ cwd: ctx.cwd, blackboard: bb, signal: currentAbortController.signal, onAgentEvent: (agentName, role, taskId, event) => { updateSubagentEvent(agentName, role, taskId, event); requestAnimationRender?.(); }, }); subagentActivities.clear(); // Done const waves = waveGroups(planOutput.tasks); const dagText = formatDAG(planOutput.tasks); const estTokens = estimatePhaseTokens(planOutput.tasks); setCurrentStatus("ready"); ctx.ui.setStatus( "morph", `morph:work (ready) [DONE] ${planOutput.tasks.length} tasks, ${waves.length} waves` ); updateWidget(ctx as any); const summary = [ `# Plan Complete`, ``, `**${planOutput.tasks.length} tasks** in **${waves.length} waves** • ~${formatTokens(estTokens)} est. tokens`, ``, `**Architecture**:`, `\`\`\`mermaid`, planOutput.architectureDiagram.slice(0, 500), `\`\`\``, ``, `**QA Strategy**: ${planOutput.qaStrategy.slice(0, 200)}`, ``, dagText, ``, `State -> \`.morph/state.json\` • Next -> /morph:work`, ].join("\n"); pi.sendMessage({ customType: "morph", content: summary, display: true, details: { phase: "plan" } }); ctx.ui.notify( `Plan done! ${planOutput.tasks.length} tasks in ${waves.length} waves. /morph:work to implement.`, "success" as any ); } catch (err: any) { setCurrentStatus("ready"); ctx.ui.setStatus("morph", `morph:plan FAILED ${err.message.slice(0, 40)}`); ctx.ui.setWidget("morph", undefined); ctx.ui.notify(`Plan failed: ${err.message}`, "error"); } }, }); // ═══════════════════════════════════════════ // WORK — with live task tracker widget // ═══════════════════════════════════════════ pi.registerCommand("morph:work", { description: "Work: execute the task DAG (Engineer + Reviewer per task, live tracker)", handler: async (_args, ctx) => { const bb = bindWorkspace(ctx.cwd); const state = bb.getState(); if (!state.planOutput) { ctx.ui.notify("No plan output. Run /morph:plan first.", "error"); return; } const specApproved = await reviewWorkSpecGate(ctx, state.planOutput); if (!specApproved) return; // ── Fix 5: Protect untracked files from git stash checkpoints ── try { const { execSync } = require("node:child_process"); execSync(`git add -A`, { cwd: ctx.cwd, timeout: 10000, stdio: "pipe" }); const status = execSync(`git status --porcelain`, { cwd: ctx.cwd, encoding: "utf-8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"], }).trim(); if (status) { execSync(`git stash push -m "morph: pre-work checkpoint" --include-untracked`, { cwd: ctx.cwd, timeout: 10000, stdio: "pipe" }); } } catch { // git unavailable — skip } bb.transition("work"); setCurrentStatus("busy"); const display = buildPipelineDisplay(); ctx.ui.setStatus("morph", buildStatusBar(display)); setPipelineWidget(ctx as any, () => buildPipelineDisplay()); ctx.ui.notify(`Work: ${state.planOutput.tasks.length} tasks, ${waveGroups(state.planOutput.tasks).length} waves...`, "info"); try { currentAbortController = new AbortController(); // Collect live task state for widget updates const liveTasks = new Map(); for (const t of state.planOutput.tasks) { liveTasks.set(t.id, { id: t.id, description: t.description, status: "pending", }); } const plannedWaves = waveGroups(state.planOutput.tasks); const originalWaveByTaskId = new Map(); plannedWaves.forEach((wave, index) => wave.forEach((task) => originalWaveByTaskId.set(task.id, index + 1))); const results = await executeWorkFlow({ cwd: ctx.cwd, blackboard: bb, maxParallel: 3, signal: currentAbortController.signal, onWaveStart: async (wave, waveIndex) => { setPipelineWidget(ctx as any, () => buildPipelineDisplay([...liveTasks.values()])); const originalWaveNumber = Math.min( ...wave.map((task) => originalWaveByTaskId.get(task.id) ?? waveIndex + 1) ); ctx.ui.notify( `Work wave ${originalWaveNumber}/${plannedWaves.length}: ${wave.length} ready task${wave.length === 1 ? "" : "s"} · auto-running in batches of up to 3.`, "info" ); return true; }, onTaskStart: (task) => { liveTasks.set(task.id, { ...liveTasks.get(task.id)!, status: "running" }); setPipelineWidget(ctx as any, () => buildPipelineDisplay([...liveTasks.values()])); }, onTaskComplete: (result) => { // Update live task status liveTasks.set(result.taskId, { ...liveTasks.get(result.taskId)!, status: result.status === "done" ? "done" : result.status === "blocked" ? "blocked" : "failed", }); // Refresh widget setPipelineWidget(ctx as any, () => buildPipelineDisplay([...liveTasks.values()])); // Update status bar const done = [...liveTasks.values()].filter((t) => t.status === "done").length; const total = liveTasks.size; ctx.ui.setStatus("morph", `morph:work ${done}/${total} tasks running...`); // Per-task notification const icon = result.status === "done" ? "[DONE]" : result.status === "blocked" ? "[BLOCKED]" : "[FAILED]"; ctx.ui.notify( `${icon} [${result.taskId}] ${result.summary.slice(0, 80)}`, result.status === "done" ? "info" : "warning" ); completeTaskFileActivities(result.taskId); clearSubagentActivity("engineer"); clearSubagentActivity("peer-reviewer"); }, onTaskActivity: (taskId, activities) => { setTaskFileActivities( taskId, activities.map((activity) => ({ ...activity, status: "active" })) ); requestAnimationRender?.(); }, onAgentEvent: (agentName, role, taskId, event) => { updateSubagentEvent(agentName, role, taskId, event); requestAnimationRender?.(); }, }); subagentActivities.clear(); const latestWorkState = bb.getState(); // Summarize outcome. Incomplete work remains in WORK; only a fully // successful DAG is allowed to advance to REVIEW. const done = results.filter((r) => r.status === "done").length; const failed = results.filter((r) => r.status === "failed").length; const blocked = results.filter((r) => r.status === "blocked").length; const totalTasks = latestWorkState.planOutput!.tasks.length; const pending = Math.max(0, totalTasks - done - failed - blocked); const incomplete = done !== totalTasks || failed > 0 || blocked > 0; const failedResults = results.filter((result) => result.status !== "done"); setCurrentStatus("ready"); ctx.ui.setStatus( "morph", incomplete ? `morph:work HALTED ${done}/${totalTasks} done` : `morph:review (ready) [DONE] ${done}/${totalTasks} done` ); setPipelineWidget(ctx as any, () => buildPipelineDisplay()); const progressText = formatProgress(results, latestWorkState.planOutput!.tasks); if (incomplete) { const diagnosis = diagnoseRecoveryState(latestWorkState); persistRecoveryReport(bb, latestWorkState, diagnosis); if (shouldAutoRecoverWithinWork(latestWorkState, diagnosis)) { clearAutoRecoverableTaskResult(bb, diagnosis); ctx.ui.notify( `Auto-recovering ${diagnosis.taskId}: ${diagnosis.failureKind}. ${diagnosis.recommendation}`, "info" ); setCurrentStatus("busy"); return await (async () => { await runMorphPipeline("", ctx as any); })(); } } const summary = [ incomplete ? `# Work Halted` : `# Work Complete`, ``, `**${done} done**${failed > 0 ? ` • ${failed} failed` : ""}${blocked > 0 ? ` • ${blocked} blocked` : ""}${pending > 0 ? ` • ${pending} pending` : ""}`, ``, progressText, ``, ...(incomplete ? [ `## Failure details`, ...failedResults.map((result) => `- **${result.taskId}** — ${result.summary}${result.filesChanged.length === 0 ? " (no files changed)" : ` (${result.filesChanged.length} file${result.filesChanged.length === 1 ? "" : "s"} changed)`}` ), ``, ] : []), incomplete ? `**Next**: inspect the failed task, then use \`/morph:recover\` or \`/morph:run\` after fixing the cause.` : `State -> \`.morph/state.json\` • Next -> /morph:review`, ].join("\n"); pi.sendMessage({ customType: "morph", content: summary, display: true, details: { phase: "work" } }); ctx.ui.notify( incomplete ? `Work halted: ${done}/${totalTasks} tasks complete. Review is blocked until work succeeds.` : `Work done! ${done}/${totalTasks} tasks. /morph:review to audit.`, (incomplete ? "warning" : "success") as any ); } catch (err: any) { subagentActivities.clear(); activeFileActivities.clear(); bb.clearActiveAgents(); setCurrentStatus("ready"); ctx.ui.setStatus("morph", `morph:work FAILED ${err.message.slice(0, 40)}`); updateWidget(ctx as any); ctx.ui.notify(`Work failed: ${err.message}`, "error"); } }, }); // ═══════════════════════════════════════════ // REVIEW // ═══════════════════════════════════════════ pi.registerCommand("morph:review", { description: "Review: audit with 4 reviewers (Tech Lead, QA, Perf, End User)", handler: async (args, ctx) => { const bb = bindWorkspace(ctx.cwd); const state = bb.getState(); if (!state.planOutput) { ctx.ui.notify("No plan output. Run /morph:run and /morph:plan first.", "error"); return; } const allowPartialReview = /\b--partial\b/.test(args || ""); const doneTaskIds = new Set(state.workResults.filter((result) => result.status === "done").map((result) => result.taskId)); const allPlannedTasksDone = state.planOutput.tasks.every((task) => doneTaskIds.has(task.id)); if (!allPlannedTasksDone && !allowPartialReview) { ctx.ui.notify( "Review blocked: work is incomplete. Finish WORK first, or use /morph:review --partial intentionally.", "warning" ); return; } bb.transition("review"); setCurrentStatus("busy"); updateWidget(ctx as any); ctx.ui.setStatus("morph", "morph:review 4 reviewers auditing..."); ctx.ui.notify("Review: Tech Lead + QA Auditor + Performance Guru + End User...", "info"); try { currentAbortController = new AbortController(); // Stage updates as agents run ctx.ui.setStatus("morph", "morph:review QA + Perf + User reviewing..."); const reviewOutput = await executeReviewFlow({ cwd: ctx.cwd, blackboard: bb, signal: currentAbortController.signal, focus: args || undefined, onAgentEvent: (agentName, role, taskId, event) => { updateSubagentEvent(agentName, role, taskId, event); requestAnimationRender?.(); }, }); subagentActivities.clear(); ctx.ui.setStatus("morph", "morph:review Tech Lead synthesizing verdict..."); const readiness = assessReviewReadiness(reviewOutput); const shipReady = allPlannedTasksDone && readiness.disposition === "ship_candidate"; // Done setCurrentStatus("ready"); ctx.ui.setStatus( "morph", shipReady ? "morph:ship (ready) [DONE]" : `morph:review (not ship-ready) [${reviewOutput.status}]` ); updateWidget(ctx as any); const summary = [ `# Review — ${reviewOutput.status}`, ``, `**Score**: ${reviewOutput.efficiencyScore}/10`, ``, `**Technical Audit**:`, reviewOutput.technicalAudit.slice(0, 400), ``, `**User Perspective**:`, reviewOutput.userPerspectiveFeedback.slice(0, 300), ``, reviewOutput.requiredChanges.length > 0 ? [ `**Changes Required** (${reviewOutput.requiredChanges.length}):`, ...reviewOutput.requiredChanges.map((c) => `- [${c.severity}] ${c.description}`), ].join("\n") : `**No changes required**`, ``, reviewOutput.securityIssues.length > 0 ? `**Security**: ${reviewOutput.securityIssues.length} issues found` : `**Security**: No issues found`, ``, shipReady ? `State -> \`.morph/state.json\` • Next -> /morph:ship` : `State -> \`.morph/state.json\` | Not ship-ready: ${readiness.reasons.join("; ") || "work is incomplete"}`, ].join("\n"); pi.sendMessage({ customType: "morph", content: summary, display: true, details: { phase: "review" } }); ctx.ui.notify( shipReady ? "Review APPROVED! /morph:ship to release." : `Review not ship-ready: ${readiness.reasons.join("; ") || "work is incomplete"}`, (shipReady ? "success" : "warning") as any ); } catch (err: any) { setCurrentStatus("ready"); ctx.ui.setStatus("morph", `morph:review FAILED ${err.message.slice(0, 40)}`); ctx.ui.setWidget("morph", undefined); ctx.ui.notify(`Review failed: ${err.message}`, "error"); } }, }); // ═══════════════════════════════════════════ // SHIP // ═══════════════════════════════════════════ pi.registerCommand("morph:ship", { description: "Ship: release with verification + changelog (DevOps + Release Consultant)", handler: async (_args, ctx) => { const bb = bindWorkspace(ctx.cwd); const state = bb.getState(); if (!state.reviewOutput) { ctx.ui.notify("Review output is missing. Run /morph:review before shipping.", "error"); return; } const readiness = assessReviewReadiness(state.reviewOutput); if (readiness.disposition !== "ship_candidate") { ctx.ui.notify(`Review is not ship-ready: ${readiness.reasons.join("; ")}.`, "error"); return; } bb.transition("ship"); setCurrentStatus("busy"); updateWidget(ctx as any); ctx.ui.setStatus("morph", "morph:ship DevOps verifying..."); ctx.ui.notify("Ship: DevOps + Release Consultant preparing release...", "info"); try { currentAbortController = new AbortController(); ctx.ui.setStatus("morph", "morph:ship DevOps + Consultant..."); const shipOutput = await executeShipFlow({ cwd: ctx.cwd, blackboard: bb, signal: currentAbortController.signal, onAgentEvent: (agentName, role, taskId, event) => { updateSubagentEvent(agentName, role, taskId, event); requestAnimationRender?.(); }, }); subagentActivities.clear(); const finalState = bb.getState(); const report = writeFinalReportArtifacts(ctx); openFileInBrowser(report.htmlPath); setCurrentStatus("ready"); ctx.ui.setStatus("morph", `morph:done [DONE] v${shipOutput.version}`); ctx.ui.setWidget("morph", ["[DONE] morph pipeline complete!", ` Version: ${shipOutput.version} • Status: ${shipOutput.status}`]); const costBreakdown = [ ` Spark: ${formatTokens(finalState.tokenLedger?.spark || 0)}`, ` Plan: ${formatTokens(finalState.tokenLedger?.plan || 0)}`, ` Work: ${formatTokens(finalState.tokenLedger?.work || 0)}`, ` Review: ${formatTokens(finalState.tokenLedger?.review || 0)}`, ` Ship: ${formatTokens(finalState.tokenLedger?.ship || 0)}`, ` ─────────────────`, ` Total: ${formatTokens(finalState.tokenLedger?.total || 0)} tokens`, ].join("\n"); const summary = [ `# Shipped — v${shipOutput.version}`, ``, `**Status**: ${shipOutput.status}`, ``, `**Changelog**:`, shipOutput.changelog, ``, `**Deployment Checklist**:`, ...shipOutput.deploymentChecklist.map((c) => `- [${c.done ? "x" : " "}] ${c.item}`), ``, shipOutput.rollbackPlan ? `**Rollback Plan**:\n${shipOutput.rollbackPlan}` : "", ``, `**Pipeline Costs**:`, costBreakdown, ``, `**Final Handoff Report**:`, `- Markdown: \`${report.markdownPath}\``, `- HTML: \`${report.htmlPath}\``, ``, `Pipeline complete!`, ].join("\n"); pi.sendMessage({ customType: "morph", content: summary, display: true, details: { phase: "ship" } }); ctx.ui.notify(`Shipped v${shipOutput.version}! Pipeline complete.`, "success" as any); } catch (err: any) { setCurrentStatus("ready"); ctx.ui.setStatus("morph", `morph:ship FAILED ${err.message.slice(0, 40)}`); ctx.ui.setWidget("morph", undefined); ctx.ui.notify(`Ship failed: ${err.message}`, "error"); } }, }); // ═══════════════════════════════════════════ // GUIDED PIPELINE // ═══════════════════════════════════════════ const runMorphPipeline = async (args: string, ctx: any) => { const bb = bindWorkspace(ctx.cwd); let state = bb.getState(); while (state.phase !== "done") { // ── Determine starting point ── if (state.phase === "idle" || state.phase === "spark") { const prompt = args?.trim() || state.pipelinePrompt?.trim(); if (!prompt) { ctx.ui.notify("Usage: /morph:run to start a new pipeline", "error"); return; } if (state.phase === "idle") { bb.setPipelinePrompt(prompt); bb.transition("spark"); } else { ctx.ui.notify("Restoring interrupted Spark phase from checkpoint...", "info"); } setCurrentStatus("busy"); updateWidget(ctx as any); ctx.ui.setStatus("morph", "morph:run Spark phase..."); ctx.ui.notify("morph pipeline started! Beginning Spark phase...", "info"); try { currentAbortController = new AbortController(); const sparkOutput = await executeSparkFlow({ cwd: ctx.cwd, prompt, blackboard: bb, signal: currentAbortController.signal, onAgentEvent: (agentName, role, taskId, event) => { updateSubagentEvent(agentName, role, taskId, event); requestAnimationRender?.(); }, }); subagentActivities.clear(); bb.setSparkOutput(sparkOutput); state = bb.getState(); setCurrentStatus("ready"); ctx.ui.setStatus("morph", "morph:run Spark complete"); updateWidget(ctx as any); ctx.ui.notify("Spark complete! Review summary below.", "success" as any); // Gate: Spark → Plan const sparkSummary = [ `# Spark Complete`, ``, `**Vision**: ${sparkOutput.visionStatement.slice(0, 300)}`, ``, `**Core Features** (${sparkOutput.coreFeatures.length}):`, ...sparkOutput.coreFeatures.map((f) => `- ${f}`), ``, `**Target User**: ${sparkOutput.targetUserPersona.slice(0, 200)}`, ``, `**Tech Stack**: ${sparkOutput.technicalStackRecommendation}`, ``, `**Risks** (${sparkOutput.risks.length}):`, ...sparkOutput.risks.slice(0, 5).map((r) => `- ${r}`), ``, `**Next**: Confirm to proceed to PLAN phase.`, ].join("\n"); pi.sendMessage({ customType: "morph", content: sparkSummary, display: true, details: { phase: "spark" } }); const proceed = await reviewSparkGate(ctx, sparkOutput); if (!proceed) { ctx.ui.notify("Pipeline paused after Spark. Run /morph:run to continue.", "info"); return; } } catch (err: any) { setCurrentStatus("ready"); ctx.ui.setStatus("morph", `morph:run Spark failed: ${err.message.slice(0, 40)}`); ctx.ui.notify(`Spark failed: ${err.message}`, "error"); return; } } // ── Plan ── if (state.phase === "plan") { setCurrentStatus("busy"); ctx.ui.setStatus("morph", "morph:run Plan phase..."); ctx.ui.notify("Plan: Architect + QA + Efficiency working...", "info"); updateWidget(ctx as any); try { currentAbortController = new AbortController(); const planOutput = await executePlanFlow({ cwd: ctx.cwd, blackboard: bb, signal: currentAbortController.signal, onAgentEvent: (agentName, role, taskId, event) => { updateSubagentEvent(agentName, role, taskId, event); requestAnimationRender?.(); }, }); subagentActivities.clear(); bb.setPlanOutput(planOutput); state = bb.getState(); const waves = waveGroups(planOutput.tasks); setCurrentStatus("ready"); ctx.ui.setStatus("morph", `morph:run ${planOutput.tasks.length} tasks planned`); updateWidget(ctx as any); ctx.ui.notify("Plan complete! Review below.", "success" as any); // Gate: Plan → Work const planSummary = [ `# Plan Complete`, ``, `**${planOutput.tasks.length} tasks** in **${waves.length} waves**`, ``, `**Architecture**:`, `\`\`\`mermaid`, planOutput.architectureDiagram.slice(0, 400), `\`\`\``, ``, `**QA Strategy**: ${planOutput.qaStrategy.slice(0, 200)}`, ``, `**Next**: Confirm to proceed to WORK phase (implementation).`, ].join("\n"); pi.sendMessage({ customType: "morph", content: planSummary, display: true, details: { phase: "plan" } }); const proceed = await reviewWorkSpecGate(ctx, planOutput); if (!proceed) { ctx.ui.notify("Pipeline paused before Work. Run /morph:run to continue.", "info"); return; } } catch (err: any) { setCurrentStatus("ready"); ctx.ui.setStatus("morph", `morph:run Plan failed: ${err.message.slice(0, 40)}`); ctx.ui.notify(`Plan failed: ${err.message}`, "error"); return; } } // ── Work ── if (state.phase === "work") { // ── Fix 5: Protect untracked files from git stash checkpoints ── try { const { execSync } = require("node:child_process"); execSync(`git add -A`, { cwd: ctx.cwd, timeout: 10000, stdio: "pipe" }); } catch { // git unavailable — skip } setCurrentStatus("busy"); ctx.ui.setStatus("morph", "morph:run Work phase..."); ctx.ui.notify("Work: executing task DAG...", "info"); updateWidget(ctx as any); try { currentAbortController = new AbortController(); const liveTasks = new Map(); for (const t of state.planOutput!.tasks) { const existingResult = state.workResults.find(r => r.taskId === t.id); liveTasks.set(t.id, { id: t.id, description: t.description, status: existingResult ? (existingResult.status === "done" ? "done" : existingResult.status === "blocked" ? "blocked" : "failed") : "pending" }); } const plannedWaves = waveGroups(state.planOutput!.tasks); const originalWaveByTaskId = new Map(); plannedWaves.forEach((wave, index) => wave.forEach((task) => originalWaveByTaskId.set(task.id, index + 1))); const results = await executeWorkFlow({ cwd: ctx.cwd, blackboard: bb, maxParallel: 3, signal: currentAbortController.signal, onWaveStart: async (wave, waveIndex) => { // If all tasks in this wave are already done (from a previous loop), skip confirm const allDone = wave.every(t => liveTasks.get(t.id)?.status === "done"); if (allDone) return true; setPipelineWidget(ctx as any, () => buildPipelineDisplay([...liveTasks.values()])); const originalWaveNumber = Math.min( ...wave.map((task) => originalWaveByTaskId.get(task.id) ?? waveIndex + 1) ); ctx.ui.notify( `Work wave ${originalWaveNumber}/${plannedWaves.length}: ${wave.length} ready task${wave.length === 1 ? "" : "s"} · auto-running in batches of up to 3.`, "info" ); return true; }, onTaskStart: (task) => { if (liveTasks.get(task.id)?.status !== "done") { liveTasks.set(task.id, { ...liveTasks.get(task.id)!, status: "running" }); } setPipelineWidget(ctx as any, () => buildPipelineDisplay([...liveTasks.values()])); }, onTaskComplete: (result) => { liveTasks.set(result.taskId, { ...liveTasks.get(result.taskId)!, status: result.status === "done" ? "done" : result.status === "blocked" ? "blocked" : "failed", }); setPipelineWidget(ctx as any, () => buildPipelineDisplay([...liveTasks.values()])); const done = [...liveTasks.values()].filter((t) => t.status === "done").length; ctx.ui.setStatus("morph", `morph:run Work: ${done}/${liveTasks.size} tasks running...`); const icon = result.status === "done" ? "[DONE]" : result.status === "blocked" ? "[BLOCKED]" : "[FAILED]"; ctx.ui.notify(`${icon} [${result.taskId}] ${result.summary.slice(0, 80)}`, result.status === "done" ? "info" : "warning"); completeTaskFileActivities(result.taskId); clearSubagentActivity("engineer"); clearSubagentActivity("peer-reviewer"); }, onTaskActivity: (taskId, activities) => { setTaskFileActivities( taskId, activities.map((activity) => ({ ...activity, status: "active" })) ); requestAnimationRender?.(); }, onAgentEvent: (agentName, role, taskId, event) => { updateSubagentEvent(agentName, role, taskId, event); requestAnimationRender?.(); }, }); subagentActivities.clear(); state = bb.getState(); const done = results.filter((r) => r.status === "done").length; const failed = results.filter((r) => r.status === "failed").length; const blocked = results.filter((r) => r.status === "blocked").length; const totalTasks = state.planOutput!.tasks.length; const pending = Math.max(0, totalTasks - done - failed - blocked); const incomplete = done !== totalTasks || failed > 0 || blocked > 0; const failedResults = results.filter((result) => result.status !== "done"); setCurrentStatus("ready"); ctx.ui.setStatus( "morph", incomplete ? `morph:run Work HALTED: ${done}/${totalTasks} done` : `morph:run Work: ${done}/${totalTasks} done` ); updateWidget(ctx as any); ctx.ui.notify( incomplete ? `Work halted: ${done}/${totalTasks} tasks complete. Review is blocked until work succeeds.` : `Work complete! ${done}/${totalTasks} tasks done.`, (incomplete ? "warning" : "success") as any ); const progressText = formatProgress(results, state.planOutput!.tasks); if (incomplete) { const diagnosis = diagnoseRecoveryState(state); persistRecoveryReport(bb, state, diagnosis); } const workSummary = [ incomplete ? `# Work Halted` : `# Work Complete`, ``, `**${done} done**${failed > 0 ? ` • ${failed} failed` : ""}${blocked > 0 ? ` • ${blocked} blocked` : ""}${pending > 0 ? ` • ${pending} pending` : ""}`, ``, progressText, ``, ...(incomplete ? [ `## Failure details`, ...failedResults.map((result) => `- **${result.taskId}** — ${result.summary}${result.filesChanged.length === 0 ? " (no files changed)" : ` (${result.filesChanged.length} file${result.filesChanged.length === 1 ? "" : "s"} changed)`}` ), ``, ] : []), incomplete ? `**Next**: inspect the failed task, then use \`/morph:recover\` or \`/morph:run\` after fixing the cause.` : `**Next**: Confirm to proceed to REVIEW phase.`, ].join("\n"); pi.sendMessage({ customType: "morph", content: workSummary, display: true, details: { phase: "work" } }); if (incomplete) { const diagnosis = diagnoseRecoveryState(state); persistRecoveryReport(bb, state, diagnosis); if (shouldAutoRecoverWithinWork(state, diagnosis)) { clearAutoRecoverableTaskResult(bb, diagnosis); state = bb.getState(); ctx.ui.notify( `Auto-recovering ${diagnosis.taskId}: ${diagnosis.failureKind}. ${diagnosis.recommendation}`, "info" ); continue; } return; } const proceed = await waitConfirm( ctx, "Proceed to Review phase?", failed > 0 ? `${failed} task(s) failed. Review failures in chat and confirm to proceed.` : `All ${done} tasks done. Confirm to start review.`, "work" ); if (!proceed) { ctx.ui.notify("Pipeline paused after Work. Run /morph:run to continue.", "info"); return; } } catch (err: any) { subagentActivities.clear(); activeFileActivities.clear(); bb.clearActiveAgents(); setCurrentStatus("ready"); ctx.ui.setStatus("morph", `morph:run Work failed: ${err.message.slice(0, 40)}`); updateWidget(ctx as any); ctx.ui.notify(`Work failed: ${err.message}`, "error"); return; } } // ── Review ── if (state.phase === "review") { if (!isWorkReadyForReview(state)) { bb.transition("work"); state = bb.getState(); ctx.ui.notify( "Review blocked: WORK is incomplete. Returning to WORK instead of auditing stale or partial state.", "warning" ); continue; } setCurrentStatus("busy"); ctx.ui.setStatus("morph", "morph:run Review phase..."); ctx.ui.notify("Review: Tech Lead + QA + Perf + End User auditing...", "info"); updateWidget(ctx as any); try { currentAbortController = new AbortController(); const reviewOutput = await executeReviewFlow({ cwd: ctx.cwd, blackboard: bb, signal: currentAbortController.signal, onAgentEvent: (agentName, role, taskId, event) => { updateSubagentEvent(agentName, role, taskId, event); requestAnimationRender?.(); }, }); subagentActivities.clear(); bb.setReviewOutput(reviewOutput); state = bb.getState(); setCurrentStatus("ready"); const verdictIcon = reviewOutput.status === "APPROVED" ? "[APPROVED]" : "[REJECTED]"; ctx.ui.setStatus("morph", `morph:run Review: ${reviewOutput.status}`); updateWidget(ctx as any); ctx.ui.notify( `Review: ${reviewOutput.status}`, (reviewOutput.status === "APPROVED" ? "success" : "warning") as any ); const readiness = assessReviewReadiness(reviewOutput); if (readiness.disposition !== "ship_candidate") { if (readiness.disposition === "auto_repair") { bb.clearWorkResults(readiness.actionableTaskIds); bb.transition("work"); ctx.ui.notify( `Review below ship floor: ${readiness.reasons.join("; ")}. Auto-recovering ${readiness.actionableTaskIds.length} targeted task${readiness.actionableTaskIds.length === 1 ? "" : "s"} inside the approved work scope.`, "info" ); state = bb.getState(); continue; } bb.transition("review"); state = bb.getState(); ctx.ui.notify( `Review below ship floor but not actionable automatically: ${readiness.reasons.join("; ")}. Opening an explicit exception gate instead of resetting completed work.`, "warning" ); const accepted = await reviewExceptionGate(ctx, readiness); if (!accepted) { ctx.ui.notify("Pipeline paused at release exception. Resolve the issue, then re-run /morph:run when ready.", "info"); return; } bb.transition("ship"); state = bb.getState(); continue; } // Gate: Review → Ship const reviewSummary = [ `# Review — ${reviewOutput.status}`, ``, `**Score**: ${reviewOutput.efficiencyScore}/10`, ``, `**Technical Audit**:`, reviewOutput.technicalAudit.slice(0, 400), ``, `**Security**: ${reviewOutput.securityIssues.length > 0 ? reviewOutput.securityIssues.length + " issues" : "No issues"}`, ``, `**Next**: Confirm to proceed to SHIP phase (deployment).`, ].join("\n"); pi.sendMessage({ customType: "morph", content: reviewSummary, display: true, details: { phase: "review" } }); const proceed = await reviewShipGate(ctx); if (!proceed) { ctx.ui.notify("Pipeline paused after Review. Run /morph:run to continue.", "info"); return; } } catch (err: any) { setCurrentStatus("ready"); ctx.ui.setStatus("morph", `morph:run Review failed: ${err.message.slice(0, 40)}`); ctx.ui.notify(`Review failed: ${err.message}`, "error"); return; } } // ── Ship ── if (state.phase === "ship") { setCurrentStatus("busy"); ctx.ui.setStatus("morph", "morph:run Ship phase..."); ctx.ui.notify("Ship: DevOps + Release Consultant preparing release...", "info"); updateWidget(ctx as any); try { currentAbortController = new AbortController(); const shipOutput = await executeShipFlow({ cwd: ctx.cwd, blackboard: bb, signal: currentAbortController.signal, onAgentEvent: (agentName, role, taskId, event) => { updateSubagentEvent(agentName, role, taskId, event); requestAnimationRender?.(); }, }); subagentActivities.clear(); bb.setShipOutput(shipOutput); state = bb.getState(); const report = writeFinalReportArtifacts(ctx); openFileInBrowser(report.htmlPath); setCurrentStatus("ready"); ctx.ui.setStatus("morph", `morph:run v${shipOutput.version} shipped`); updateWidget(ctx as any); pi.sendMessage({ customType: "morph", content: `# Final Handoff Report\n\nGenerated after ship:\n- Markdown: \`${report.markdownPath}\`\n- HTML: \`${report.htmlPath}\``, display: true, details: { phase: "ship", markdownPath: report.markdownPath, htmlPath: report.htmlPath }, }); ctx.ui.notify(`Shipped v${shipOutput.version}! Final report generated.`, "success" as any); } catch (err: any) { setCurrentStatus("ready"); ctx.ui.setStatus("morph", `morph:run Ship failed: ${err.message.slice(0, 40)}`); ctx.ui.notify(`Ship failed: ${err.message}`, "error"); return; } } } // ── Done ── ctx.ui.notify("morph pipeline complete! /morph:status for details.", "success" as any); }; pi.registerCommand("morph:run", { description: "Run the full pipeline with review gates: spark -> plan -> work -> review -> ship", handler: runMorphPipeline, }); // ═══════════════════════════════════════════ // UTILITY COMMANDS // ═══════════════════════════════════════════ pi.registerCommand("morph:config", { description: "Configure the model for morph agents (e.g. /morph:config provider=anthropic model=claude-3-5-sonnet-20241022, or /morph:config inherit)", handler: async (args, ctx) => { const bb = bindWorkspace(ctx.cwd); if (!args.trim()) { const conf = bb.getState().config; ctx.ui.notify(`Current morph config: provider=${conf?.provider || "default"}, model=${conf?.model || "default"}, source=${conf?.source || "inherited"}`, "info"); return; } const previousConfig = { ...bb.getState().config }; const normalizedArgs = args.trim().toLowerCase(); const followParent = normalizedArgs === "inherit" || normalizedArgs === "follow=parent" || normalizedArgs === "source=inherited"; const newConfig: any = {}; if (followParent) { const model = ctx.model; newConfig.provider = model?.provider ? String(model.provider) : undefined; newConfig.model = model?.id ? String(model.id) : undefined; newConfig.source = "inherited"; } else { for (const part of args.split(" ")) { const [k, v] = part.split("="); if (k && v) newConfig[k] = v; } newConfig.source = "manual"; } bb.setConfig(newConfig); const currentConfig = bb.getState().config; const providerChanged = currentConfig.provider !== previousConfig.provider; const modelChanged = currentConfig.model !== previousConfig.model; const staleEnvironmentFailuresCleared = providerChanged || modelChanged ? clearStaleEnvironmentFailures(bb) : 0; if (staleEnvironmentFailuresCleared > 0) { ctx.ui.notify( `Updated morph config: provider=${currentConfig?.provider || "default"}, model=${currentConfig?.model || "default"}, source=${currentConfig?.source || "inherited"}. Cleared ${staleEnvironmentFailuresCleared} stale environment-failure result${staleEnvironmentFailuresCleared === 1 ? "" : "s"} so the next run retries against the new config.`, "success" as any ); return; } ctx.ui.notify(`Updated morph config: provider=${currentConfig?.provider || "default"}, model=${currentConfig?.model || "default"}, source=${currentConfig?.source || "inherited"}`, "success" as any); } }); pi.registerCommand("morph:status", { description: "Show morph pipeline status (widget + detailed summary)", handler: async (_args, ctx) => { const bb = bindWorkspace(ctx.cwd); const state = bb.getState(); if (state.phase === "idle") { ctx.ui.notify("morph: idle — /morph:run to begin", "info"); ctx.ui.setWidget("morph", ["morph — Ready", " /morph:run to begin"]); return; } // Update widget + status updateWidget(ctx as any); ctx.ui.setStatus("morph", buildStatusBar(buildPipelineDisplay())); // Send detailed summary as message const summary = bb.getContextualSummary(4000); pi.sendMessage({ customType: "morph", content: summary, display: true, details: { phase: "status" } }); const checkpointCount = Object.keys(state.flowCheckpoints[state.phase] || {}).length; ctx.ui.notify( checkpointCount > 0 ? `morph: phase=${state.phase} • ${checkpointCount} checkpoint${checkpointCount === 1 ? "" : "s"} available • ${formatTokens(state.tokenLedger?.total || 0)} tokens` : `morph: phase=${state.phase} • ${formatTokens(state.tokenLedger?.total || 0)} tokens`, "info" ); }, }); pi.registerCommand("morph:recover", { description: "Resume the pipeline from the last safe checkpoint", handler: async (_args, ctx) => { const bb = bindWorkspace(ctx.cwd); let state = bb.getState(); if (state.phase === "idle") { ctx.ui.notify("Nothing to recover yet. Start with /morph:run .", "info"); return; } if (state.phase === "done") { ctx.ui.notify("Pipeline already complete. Nothing to recover.", "info"); return; } const regressionRecovery = restoreTrustedRegressionSnapshot(bb, state); if (regressionRecovery.restored) { state = regressionRecovery.state; ctx.ui.notify( `State regression detected; restored the last safe work snapshot (${regressionRecovery.restoredTaskCount} completed task${regressionRecovery.restoredTaskCount === 1 ? "" : "s"} recovered).`, "warning" ); } const diagnosis = diagnoseRecoveryState(state); const reportPath = persistRecoveryReport(bb, state, diagnosis); if (diagnosis.taskId && diagnosis.failureKind === "STATE_INCONSISTENT") { bb.clearWorkResults([diagnosis.taskId]); state = bb.getState(); } else if ( diagnosis.taskId && state.phase === "work" && state.workResults.some((result) => result.taskId === diagnosis.taskId && result.status !== "done") ) { state = prepareExplicitWorkRecovery(bb, state, diagnosis); } const checkpointCount = Object.keys(state.flowCheckpoints[state.phase] || {}).length; const recoveryBrief = [ `Recovery diagnosis${diagnosis.taskId ? ` for ${diagnosis.taskId}` : ""}: ${diagnosis.failureKind || "RESUME"}`, diagnosis.evidence.length > 0 ? `Evidence: ${diagnosis.evidence.join(" ")}` : "", `Next move: ${diagnosis.recommendation}`, reportPath ? `Report: ${reportPath}` : "", ].filter(Boolean).join(" "); ctx.ui.notify( checkpointCount > 0 ? `${recoveryBrief} Recovering ${state.phase} from ${checkpointCount} checkpoint${checkpointCount === 1 ? "" : "s"}...` : `${recoveryBrief} No saved checkpoint in ${state.phase}; resuming from the start of the phase...`, "info" ); await runMorphPipeline("", ctx as any); }, }); pi.registerCommand("morph:reset", { description: "Reset pipeline (full or to a phase: idle/plan/work/review)", handler: async (args, ctx) => { const phase = args?.trim().toLowerCase() || "full"; if (phase === "full") { const ok = await ctx.ui.confirm("Reset entire morph pipeline?", "All state in .morph/ will be deleted."); if (!ok) return; deletePipelineState(ctx.cwd); ctx.ui.setStatus("morph", undefined); ctx.ui.setWidget("morph", undefined); ctx.ui.notify("Pipeline reset; .morph state deleted.", "info"); return; } const bb = bindWorkspace(ctx.cwd); const valid = ["idle", "plan", "work", "review"]; if (!valid.includes(phase)) { ctx.ui.notify(`Invalid phase. Use: ${valid.join(", ")} or "full"`, "error"); return; } const ok = await ctx.ui.confirm(`Reset to "${phase}"?`, `All state from ${phase} onward will be cleared.`); if (!ok) return; if (phase === "idle") deletePipelineState(ctx.cwd); else bb.resetPhase(phase as any); ctx.ui.setStatus("morph", `morph:${bb.getState().phase}`); updateWidget(ctx as any); ctx.ui.notify(`Reset to: ${bb.getState().phase}`, "info"); }, }); pi.registerCommand("morph:team", { description: "Show morph agent team composition", handler: async (_args, ctx) => { const lines = [ `# morph — Agent Teams (${SPARK_AGENTS.length + PLAN_AGENTS.length + WORK_AGENTS.length + REVIEW_AGENTS.length + SHIP_AGENTS.length} agents)`, "", "## Spark (2)", ...SPARK_AGENTS.map((a) => `- **${a.role}** \`${a.name}\` — ${a.description}`), "", "## Plan (3)", ...PLAN_AGENTS.map((a) => `- **${a.role}** \`${a.name}\` — ${a.description}`), "", "## Work (2 per task)", ...WORK_AGENTS.map((a) => `- **${a.role}** \`${a.name}\` — ${a.description}`), "", "## Review (4)", ...REVIEW_AGENTS.map((a) => `- **${a.role}** \`${a.name}\` — ${a.description}`), "", "## Ship (2)", ...SHIP_AGENTS.map((a) => `- **${a.role}** \`${a.name}\` — ${a.description}`), ].join("\n"); pi.sendMessage({ customType: "morph", content: lines, display: true }); }, }); pi.registerCommand("morph:web", { description: "Start the morph Mission Control Web UI", handler: async (_args, ctx) => { if (webServer) { ctx.ui.notify("Mission Control is already running at http://localhost:4040", "info" as any); return; } try { const bb = bindWorkspace(ctx.cwd); webServer = startMorphServer(bb, 4040); ctx.ui.notify("Mission Control started at http://localhost:4040", "success" as any); } catch (err: any) { ctx.ui.notify(`Failed to start server: ${err.message}`, "error" as any); } }, }); }