| // src/validators.ts | ||
| function isObject(v) { | ||
| return typeof v === "object" && v !== null && !Array.isArray(v); | ||
| } | ||
| function isString(v) { | ||
| return typeof v === "string"; | ||
| } | ||
| function isArray(v) { | ||
| return Array.isArray(v); | ||
| } | ||
| function fail(issues) { | ||
| return { ok: false, issues }; | ||
| } | ||
| function pass(value) { | ||
| return { ok: true, issues: [], value }; | ||
| } | ||
| function validateWorkItems(input) { | ||
| if (!isArray(input)) return fail(["work items must be an array"]); | ||
| if (input.length === 0) return fail(["work items must not be empty"]); | ||
| const issues = []; | ||
| for (let i = 0; i < input.length; i++) { | ||
| const item = input[i]; | ||
| if (!isObject(item)) { | ||
| issues.push(`items[${i}] must be an object`); | ||
| continue; | ||
| } | ||
| if (!isString(item.id)) issues.push(`items[${i}].id must be a string`); | ||
| if (!isString(item.role)) issues.push(`items[${i}].role must be a string`); | ||
| if (!isString(item.task)) issues.push(`items[${i}].task must be a string`); | ||
| } | ||
| if (issues.length > 0) return fail(issues); | ||
| return pass(input); | ||
| } | ||
| function allItemsHaveResults(items) { | ||
| return items.every((item) => item.result !== void 0); | ||
| } | ||
| function validateClarifyOutput(input) { | ||
| const issues = []; | ||
| if (!isObject(input)) return fail(["clarify output must be an object"]); | ||
| if (typeof input.ready !== "boolean") issues.push("ready must be a boolean"); | ||
| if (!isString(input.summary) || input.summary.length === 0) issues.push("summary must be a non-empty string"); | ||
| if (!isArray(input.assumptions)) issues.push("assumptions must be an array"); | ||
| if (!isArray(input.decisions)) issues.push("decisions must be an array"); | ||
| if (!input.ready && isArray(input.decisions) && input.decisions.length === 0) { | ||
| issues.push("ready=false requires at least one decision"); | ||
| } | ||
| if (isArray(input.decisions)) { | ||
| for (let i = 0; i < input.decisions.length; i++) { | ||
| const d = input.decisions[i]; | ||
| if (!isObject(d)) { | ||
| issues.push(`decisions[${i}] must be an object`); | ||
| continue; | ||
| } | ||
| if (!isString(d.id)) issues.push(`decisions[${i}].id must be a string`); | ||
| if (!isString(d.question)) issues.push(`decisions[${i}].question must be a string`); | ||
| if (!isArray(d.options) || d.options.length === 0) issues.push(`decisions[${i}].options must be a non-empty array`); | ||
| } | ||
| } | ||
| if (issues.length > 0) return fail(issues); | ||
| return pass(input); | ||
| } | ||
| function validatePlanOutput(input) { | ||
| const issues = []; | ||
| if (!isObject(input)) return fail(["plan output must be an object"]); | ||
| if (!isArray(input.tasks) || input.tasks.length === 0) { | ||
| issues.push("tasks must be a non-empty array"); | ||
| } else { | ||
| for (let i = 0; i < input.tasks.length; i++) { | ||
| const t = input.tasks[i]; | ||
| if (!isObject(t)) { | ||
| issues.push(`tasks[${i}] must be an object`); | ||
| continue; | ||
| } | ||
| if (!isString(t.id)) issues.push(`tasks[${i}].id must be a string`); | ||
| if (!isString(t.title)) issues.push(`tasks[${i}].title must be a string`); | ||
| if (!isArray(t.scope) && !isString(t.scope)) issues.push(`tasks[${i}].scope must be an array or string`); | ||
| } | ||
| } | ||
| if (!isString(input.approach) || input.approach.length === 0) issues.push("approach must be a non-empty string"); | ||
| if (!isArray(input.risks)) issues.push("risks must be an array"); | ||
| if (issues.length > 0) return fail(issues); | ||
| return pass(input); | ||
| } | ||
| function validateExecuteOutput(input) { | ||
| const issues = []; | ||
| if (!isObject(input)) return fail(["execute output must be an object"]); | ||
| if (!isString(input.summary) || input.summary.length === 0) issues.push("summary must be a non-empty string"); | ||
| if (!isArray(input.changedFiles)) issues.push("changedFiles must be an array"); | ||
| if (!isArray(input.notes)) issues.push("notes must be an array"); | ||
| if (issues.length > 0) return fail(issues); | ||
| return pass(input); | ||
| } | ||
| function validateVerifyOutput(input) { | ||
| const issues = []; | ||
| if (!isObject(input)) return fail(["verify output must be an object"]); | ||
| if (typeof input.passed !== "boolean") issues.push("passed must be a boolean"); | ||
| if (!isArray(input.issues)) issues.push("issues must be an array"); | ||
| if (!isString(input.summary) || input.summary.length === 0) issues.push("summary must be a non-empty string"); | ||
| if (isArray(input.issues)) { | ||
| for (let i = 0; i < input.issues.length; i++) { | ||
| const issue = input.issues[i]; | ||
| if (!isObject(issue)) { | ||
| issues.push(`issues[${i}] must be an object`); | ||
| continue; | ||
| } | ||
| if (!isString(issue.severity) || !["error", "warning"].includes(issue.severity)) { | ||
| issues.push(`issues[${i}].severity must be "error" or "warning"`); | ||
| } | ||
| if (!isString(issue.description)) issues.push(`issues[${i}].description must be a string`); | ||
| } | ||
| } | ||
| if (issues.length > 0) return fail(issues); | ||
| return pass(input); | ||
| } | ||
| function validateDecisionAnswers(input) { | ||
| if (!isArray(input)) return fail(["decision answers must be an array"]); | ||
| const issues = []; | ||
| for (let i = 0; i < input.length; i++) { | ||
| const a = input[i]; | ||
| if (!isObject(a)) { | ||
| issues.push(`answers[${i}] must be an object`); | ||
| continue; | ||
| } | ||
| if (!isString(a.decisionId)) issues.push(`answers[${i}].decisionId must be a string`); | ||
| if (!isString(a.selectedOptionId)) issues.push(`answers[${i}].selectedOptionId must be a string`); | ||
| } | ||
| if (issues.length > 0) return fail(issues); | ||
| return pass(input); | ||
| } | ||
| function validateWorkflowState(state) { | ||
| const issues = []; | ||
| if (!isObject(state)) return fail(["state must be an object"]); | ||
| if (!isString(state.schemaVersion)) issues.push("schemaVersion required"); | ||
| if (!isString(state.workflowId)) issues.push("workflowId required"); | ||
| if (!isString(state.description)) issues.push("description required"); | ||
| if (!isString(state.status)) issues.push("status required"); | ||
| if (!isString(state.phase)) issues.push("phase required"); | ||
| if (!isArray(state.pendingDecisions)) issues.push("pendingDecisions must be an array"); | ||
| if (!isArray(state.decisionHistory)) issues.push("decisionHistory must be an array"); | ||
| if (!isArray(state.tasks)) issues.push("tasks must be an array"); | ||
| if (typeof state.escalateAfter !== "number") issues.push("escalateAfter must be a number"); | ||
| if (!isString(state.createdAt)) issues.push("createdAt required"); | ||
| if (!isString(state.updatedAt)) issues.push("updatedAt required"); | ||
| if (issues.length > 0) return fail(issues); | ||
| return pass(state); | ||
| } | ||
| // src/prompts.ts | ||
| function workItemsOutputFormat(phaseOutputExample) { | ||
| return ` | ||
| ## Output Format | ||
| Always return a JSON array of work items: | ||
| \`\`\`json | ||
| [ | ||
| { "id": "string", "role": "string", "task": "string", "result": <phase output or omit> } | ||
| ] | ||
| \`\`\` | ||
| **Simple case** \u2014 you do it yourself, return 1 item with result: | ||
| \`\`\`json | ||
| [{ "id": "main", "role": "main", "task": "did everything", "result": ${phaseOutputExample} }] | ||
| \`\`\` | ||
| **Complex case** \u2014 too big for one agent, return multiple items WITHOUT result: | ||
| \`\`\`json | ||
| [ | ||
| { "id": "s1", "role": "structure-analyst", "task": "analyze project structure and entry points" }, | ||
| { "id": "s2", "role": "dependency-analyst", "task": "analyze internal and external dependencies" } | ||
| ] | ||
| \`\`\` | ||
| These will be executed in parallel by separate agents. Results will be synthesized afterward. | ||
| Choose based on complexity. Most tasks need just 1 item.`; | ||
| } | ||
| function buildClarifyPrompt(state) { | ||
| const parts = []; | ||
| parts.push(`# Clarify Phase`); | ||
| parts.push(``); | ||
| parts.push(`## Goal`); | ||
| parts.push(`User request: "${state.description}"`); | ||
| parts.push(``); | ||
| parts.push(`## Your task`); | ||
| parts.push(`Make this request concrete enough to plan and execute safely.`); | ||
| parts.push(``); | ||
| if (state.decisionHistory.length > 0) { | ||
| parts.push(`## Previous decisions`); | ||
| for (const d of state.decisionHistory) { | ||
| parts.push(`- ${d.decisionId}: ${d.selectedOptionId}${d.customInput ? ` (${d.customInput})` : ""}`); | ||
| } | ||
| parts.push(``); | ||
| } | ||
| parts.push(`## Step 1: Understand`); | ||
| parts.push(`Restate what the user wants in your own words. Identify the core intent.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 2: Assess`); | ||
| parts.push(`Explore the codebase. Find relevant files, patterns, constraints, and dependencies.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 3: Tighten`); | ||
| parts.push(`Identify assumptions you're making. Surface any gaps or ambiguities.`); | ||
| parts.push(`If information is missing and you cannot proceed safely, define decisions for the user.`); | ||
| parts.push(`If everything is clear enough, mark as ready.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 4: Report`); | ||
| parts.push(workItemsOutputFormat(`{ready, summary, assumptions, verifyFocus, decisions}`)); | ||
| return parts.join("\n"); | ||
| } | ||
| function buildPlanPrompt(state) { | ||
| const parts = []; | ||
| parts.push(`# Plan Phase`); | ||
| parts.push(``); | ||
| parts.push(`## Goal`); | ||
| parts.push(`User request: "${state.description}"`); | ||
| parts.push(``); | ||
| if (state.clarifyOutput) { | ||
| parts.push(`## Clarify summary`); | ||
| parts.push(state.clarifyOutput.summary); | ||
| parts.push(``); | ||
| if (state.clarifyOutput.assumptions.length > 0) { | ||
| parts.push(`## Assumptions`); | ||
| for (const a of state.clarifyOutput.assumptions) { | ||
| parts.push(`- ${a}`); | ||
| } | ||
| parts.push(``); | ||
| } | ||
| } | ||
| const adjustments = state.decisionHistory.filter((d) => d.decisionId === "plan-adjustment"); | ||
| if (adjustments.length > 0) { | ||
| parts.push(`## Plan adjustment feedback`); | ||
| for (const a of adjustments) { | ||
| parts.push(`- ${a.customInput}`); | ||
| } | ||
| parts.push(``); | ||
| } | ||
| parts.push(`## Your task`); | ||
| parts.push(`Design an approach and break it into concrete, ordered tasks.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 1: Understand`); | ||
| parts.push(`Review the clarify output and assumptions. Understand what needs to happen.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 2: Assess`); | ||
| parts.push(`Identify files, modules, and dependencies involved. Check feasibility.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 3: Design`); | ||
| parts.push(`Break the work into bounded tasks. Each task should:`); | ||
| parts.push(`- Have a clear, narrow scope (specific files/modules)`); | ||
| parts.push(`- Be independently verifiable`); | ||
| parts.push(`- List what context it needs from earlier tasks`); | ||
| parts.push(`- Declare dependencies on other tasks`); | ||
| parts.push(``); | ||
| parts.push(`## Step 4: Report`); | ||
| parts.push(workItemsOutputFormat(`{approach, risks, tasks: [{id, title, scope, context, dependsOn}]}`)); | ||
| return parts.join("\n"); | ||
| } | ||
| function buildExecutePrompt(state, task) { | ||
| const parts = []; | ||
| parts.push(`# Execute Phase \u2014 Task: ${task.title}`); | ||
| parts.push(``); | ||
| parts.push(`## Goal`); | ||
| parts.push(`User request: "${state.description}"`); | ||
| parts.push(``); | ||
| parts.push(`## Task scope`); | ||
| parts.push(`Files/modules to modify: ${task.scope.join(", ") || "as needed"}`); | ||
| parts.push(``); | ||
| parts.push(`## Task context`); | ||
| const ctx = Array.isArray(task.context) ? task.context : task.context ? [task.context] : []; | ||
| parts.push(`Files/data to read: ${ctx.join(", ") || "as needed"}`); | ||
| parts.push(``); | ||
| if (state.clarifyOutput) { | ||
| parts.push(`## Clarify summary`); | ||
| parts.push(state.clarifyOutput.summary); | ||
| parts.push(``); | ||
| } | ||
| if (state.planOutput) { | ||
| parts.push(`## Approach`); | ||
| parts.push(state.planOutput.approach); | ||
| parts.push(``); | ||
| } | ||
| if (task.verifyOutput && !task.verifyOutput.passed) { | ||
| parts.push(`## Previous verification feedback`); | ||
| parts.push(task.verifyOutput.summary); | ||
| if (task.verifyOutput.retryHint) { | ||
| parts.push(`Retry hint: ${task.verifyOutput.retryHint}`); | ||
| } | ||
| for (const issue of task.verifyOutput.issues) { | ||
| parts.push(`- [${issue.severity}] ${issue.description}${issue.suggestion ? ` \u2192 ${issue.suggestion}` : ""}`); | ||
| } | ||
| parts.push(``); | ||
| } | ||
| parts.push(`## Your task`); | ||
| parts.push(`Implement the changes described in this task.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 1: Understand`); | ||
| parts.push(`Review what this task must produce. Check the scope and constraints.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 2: Assess`); | ||
| parts.push(`Read the target files. Understand the current state before making changes.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 3: Implement`); | ||
| parts.push(`Make the changes. Stay within scope.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 4: Report`); | ||
| parts.push(workItemsOutputFormat(`{summary, changedFiles, notes}`)); | ||
| return parts.join("\n"); | ||
| } | ||
| function buildVerifyPrompt(state, task) { | ||
| const parts = []; | ||
| parts.push(`# Verify Phase \u2014 Task: ${task.title}`); | ||
| parts.push(``); | ||
| parts.push(`## Goal`); | ||
| parts.push(`Determine if the task was completed correctly.`); | ||
| parts.push(``); | ||
| if (task.executeOutput) { | ||
| parts.push(`## What was done`); | ||
| parts.push(task.executeOutput.summary); | ||
| parts.push(``); | ||
| parts.push(`## Changed files`); | ||
| for (const f of task.executeOutput.changedFiles) { | ||
| parts.push(`- ${f}`); | ||
| } | ||
| parts.push(``); | ||
| if (task.executeOutput.notes.length > 0) { | ||
| parts.push(`## Notes from executor`); | ||
| for (const n of task.executeOutput.notes) { | ||
| parts.push(`- ${n}`); | ||
| } | ||
| parts.push(``); | ||
| } | ||
| } | ||
| if (state.clarifyOutput?.verifyFocus && state.clarifyOutput.verifyFocus.length > 0) { | ||
| parts.push(`## Verify focus`); | ||
| for (const f of state.clarifyOutput.verifyFocus) { | ||
| parts.push(`- ${f}`); | ||
| } | ||
| parts.push(``); | ||
| } | ||
| parts.push(`## Attempt ${task.verifyAttempts}`); | ||
| parts.push(``); | ||
| parts.push(`## Your task`); | ||
| parts.push(`Try to disprove the claimed result. Report what you find.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 1: Understand`); | ||
| parts.push(`Review what was claimed to be done.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 2: Assess`); | ||
| parts.push(`Read the changed files. Run relevant checks or tests if applicable.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 3: Validate`); | ||
| parts.push(`Check each claim against evidence. Look for missing pieces, bugs, or regressions.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 4: Report`); | ||
| parts.push(workItemsOutputFormat(`{passed, issues: [{severity, description, suggestion?}], summary, retryHint?}`)); | ||
| return parts.join("\n"); | ||
| } | ||
| // src/instructions.ts | ||
| var _prefix; | ||
| function cliPrefix() { | ||
| if (_prefix) return _prefix; | ||
| const arg1 = process.argv[1] ?? ""; | ||
| if (arg1.includes("cli.ts") || arg1.includes("cli.js")) { | ||
| _prefix = `node ${arg1}`; | ||
| } else { | ||
| _prefix = "krow"; | ||
| } | ||
| return _prefix; | ||
| } | ||
| function submitWorkItemsCommand(workflowId, phase, taskId) { | ||
| const taskArg = taskId ? ` --task ${taskId}` : ""; | ||
| return `${cliPrefix()} submit ${workflowId} ${phase}${taskArg} '<JSON>'`; | ||
| } | ||
| function submitWorkItemResultCommand(workflowId, itemId) { | ||
| return `${cliPrefix()} submit-item ${workflowId} ${itemId} '<RESULT>'`; | ||
| } | ||
| function submitSynthesizedCommand(workflowId) { | ||
| return `${cliPrefix()} submit-synthesized ${workflowId} '<JSON>'`; | ||
| } | ||
| function decideCommand(workflowId) { | ||
| return `${cliPrefix()} decide ${workflowId} '<JSON>'`; | ||
| } | ||
| function approvePlanCommand(workflowId) { | ||
| return `${cliPrefix()} approve-plan ${workflowId}`; | ||
| } | ||
| function adjustPlanCommand(workflowId) { | ||
| return `${cliPrefix()} adjust-plan ${workflowId} '<feedback>'`; | ||
| } | ||
| function resolveVerifyCommand(workflowId, taskId, action) { | ||
| return `${cliPrefix()} resolve-verify ${workflowId} ${taskId} ${action}`; | ||
| } | ||
| function phaseInstructions(phase, onComplete) { | ||
| return [ | ||
| `Execute the prompt for the "${phase}" phase.`, | ||
| ``, | ||
| `CRITICAL OUTPUT REQUIREMENT: Your response MUST be a JSON array of work items. No prose, no markdown outside JSON, no explanation.`, | ||
| `Schema: [{ "id": string, "role": string, "task": string, "result"?: object }]`, | ||
| ``, | ||
| `Simple case (you do it yourself) \u2014 return 1 item with result:`, | ||
| `[{ "id": "main", "role": "main", "task": "did everything", "result": { ... } }]`, | ||
| ``, | ||
| `Complex case (needs parallel agents) \u2014 return multiple items WITHOUT result:`, | ||
| `[{ "id": "s1", "role": "analyst", "task": "..." }, { "id": "s2", "role": "reviewer", "task": "..." }]`, | ||
| ``, | ||
| `Then run: ${onComplete}` | ||
| ].join("\n"); | ||
| } | ||
| function gateClarifyInstructions(onComplete) { | ||
| return [ | ||
| `Present each decision to the user and collect their answers.`, | ||
| `Then run: ${onComplete}`, | ||
| `Replace '<JSON>' with an array of { decisionId, selectedOptionId, customInput? }.` | ||
| ].join("\n"); | ||
| } | ||
| function gatePlanInstructions(approveCmd, adjustCmd) { | ||
| return [ | ||
| `Present the plan to the user for review.`, | ||
| `If approved, run: ${approveCmd}`, | ||
| `If adjustments needed, run: ${adjustCmd}` | ||
| ].join("\n"); | ||
| } | ||
| function gateVerifyInstructions(taskTitle, attempts, continueCmd, stopCmd) { | ||
| return [ | ||
| `Task "${taskTitle}" has failed verification ${attempts} time(s).`, | ||
| `Present the issues to the user.`, | ||
| `Keep trying: ${continueCmd}`, | ||
| `Stop this task: ${stopCmd}` | ||
| ].join("\n"); | ||
| } | ||
| function batchInstructions(count) { | ||
| return [ | ||
| `${count} items to execute in parallel.`, | ||
| `Execute each independently. Submit each result separately using its onComplete command.` | ||
| ].join("\n"); | ||
| } | ||
| function synthesizeInstructions(phase, onComplete) { | ||
| return [ | ||
| `All work items completed. Synthesize the results into a single ${phase} output.`, | ||
| `Then run: ${onComplete}` | ||
| ].join("\n"); | ||
| } | ||
| function doneInstructions() { | ||
| return "The workflow has reached a terminal state. Report the result to the user."; | ||
| } | ||
| function faultInstructions(recoverable) { | ||
| if (recoverable) return "A recoverable error occurred. Review the issues and retry."; | ||
| return "An unrecoverable error occurred. Report the error to the user."; | ||
| } | ||
| // src/responses.ts | ||
| var PHASE_CAPABILITY = { | ||
| clarify: "read", | ||
| plan: "read", | ||
| execute: "write", | ||
| verify: "read" | ||
| }; | ||
| function clarifyContext() { | ||
| return { isolation: "isolated", inputs: [] }; | ||
| } | ||
| function planContext(state) { | ||
| const inputs = []; | ||
| if (state.clarifyOutput) inputs.push({ type: "data", ref: "clarifyOutput" }); | ||
| return { isolation: "isolated", inputs }; | ||
| } | ||
| function executeContext(state, task) { | ||
| const inputs = []; | ||
| if (state.clarifyOutput) inputs.push({ type: "data", ref: "clarifyOutput" }); | ||
| if (state.planOutput) inputs.push({ type: "data", ref: "planOutput.approach" }); | ||
| const ctx = Array.isArray(task.context) ? task.context : task.context ? [task.context] : []; | ||
| for (const c of ctx) inputs.push({ type: "file", ref: c }); | ||
| return { isolation: "isolated", inputs }; | ||
| } | ||
| function verifyContext(state, task) { | ||
| const inputs = []; | ||
| if (task.executeOutput) { | ||
| inputs.push({ type: "data", ref: `tasks.${task.id}.executeOutput` }); | ||
| for (const f of task.executeOutput.changedFiles) inputs.push({ type: "file", ref: f }); | ||
| } | ||
| if (state.clarifyOutput?.verifyFocus) inputs.push({ type: "data", ref: "clarifyOutput.verifyFocus" }); | ||
| return { isolation: "isolated", inputs }; | ||
| } | ||
| function outputSpec(phase) { | ||
| return { schema: `${phase}-output`, format: phase === "execute" ? "files" : "json" }; | ||
| } | ||
| function buildPromptForTask(state, phase, task) { | ||
| switch (phase) { | ||
| case "execute": | ||
| return buildExecutePrompt(state, task); | ||
| case "verify": | ||
| return buildVerifyPrompt(state, task); | ||
| default: | ||
| return ""; | ||
| } | ||
| } | ||
| function contextForTask(state, phase, task) { | ||
| switch (phase) { | ||
| case "execute": | ||
| return executeContext(state, task); | ||
| case "verify": | ||
| return verifyContext(state, task); | ||
| default: | ||
| return { isolation: "isolated", inputs: [] }; | ||
| } | ||
| } | ||
| function buildPhaseResponse(state, phase, taskId) { | ||
| if (phase === "clarify" || phase === "plan") { | ||
| const onComplete2 = submitWorkItemsCommand(state.workflowId, phase); | ||
| return { | ||
| type: "phase", | ||
| phase, | ||
| workflowId: state.workflowId, | ||
| prompt: phase === "clarify" ? buildClarifyPrompt(state) : buildPlanPrompt(state), | ||
| capability: PHASE_CAPABILITY[phase], | ||
| context: phase === "clarify" ? clarifyContext() : planContext(state), | ||
| output: outputSpec(phase), | ||
| onComplete: onComplete2, | ||
| instructions: phaseInstructions(phase, onComplete2) | ||
| }; | ||
| } | ||
| const task = taskId ? state.tasks.find((t) => t.id === taskId) : void 0; | ||
| const onComplete = submitWorkItemsCommand(state.workflowId, phase, taskId); | ||
| if (!task) { | ||
| return { | ||
| type: "phase", | ||
| phase, | ||
| workflowId: state.workflowId, | ||
| prompt: `Error: no task found for ${phase} phase.`, | ||
| capability: PHASE_CAPABILITY[phase], | ||
| context: { isolation: "isolated", inputs: [] }, | ||
| output: outputSpec(phase), | ||
| onComplete, | ||
| instructions: phaseInstructions(phase, onComplete) | ||
| }; | ||
| } | ||
| return { | ||
| type: "phase", | ||
| phase, | ||
| workflowId: state.workflowId, | ||
| taskId: task.id, | ||
| prompt: buildPromptForTask(state, phase, task), | ||
| capability: PHASE_CAPABILITY[phase], | ||
| context: contextForTask(state, phase, task), | ||
| output: outputSpec(phase), | ||
| onComplete, | ||
| instructions: phaseInstructions(phase, onComplete) | ||
| }; | ||
| } | ||
| function buildBatchResponse(state, items) { | ||
| const responses = items.map((item) => buildPhaseResponse(state, item.phase, item.taskId)); | ||
| return { | ||
| type: "batch", | ||
| workflowId: state.workflowId, | ||
| responses, | ||
| instructions: batchInstructions(responses.length) | ||
| }; | ||
| } | ||
| function buildWorkItemBatchResponse(state, work) { | ||
| const running = work.items.filter((i) => i.status === "running" || i.status === "pending"); | ||
| const responses = running.map((item) => { | ||
| const onComplete = submitWorkItemResultCommand(state.workflowId, item.id); | ||
| return { | ||
| type: "phase", | ||
| phase: work.phase, | ||
| workflowId: state.workflowId, | ||
| taskId: work.taskId, | ||
| prompt: `# Work Item: ${item.role} | ||
| ${item.task}`, | ||
| capability: PHASE_CAPABILITY[work.phase], | ||
| context: { isolation: "isolated", inputs: [] }, | ||
| output: { schema: "work-item-result", format: "json" }, | ||
| onComplete, | ||
| instructions: `Execute this work item. Return your result as JSON (no prose, no markdown). Then run: ${onComplete}` | ||
| }; | ||
| }); | ||
| return { | ||
| type: "batch", | ||
| workflowId: state.workflowId, | ||
| responses, | ||
| instructions: batchInstructions(responses.length) | ||
| }; | ||
| } | ||
| function buildSynthesizeResponse(state, work) { | ||
| const results = work.items.map((i) => `### ${i.role} | ||
| ${i.result ?? "(no result)"}`).join("\n\n"); | ||
| const onComplete = submitSynthesizedCommand(state.workflowId); | ||
| return { | ||
| type: "phase", | ||
| phase: work.phase, | ||
| workflowId: state.workflowId, | ||
| taskId: work.taskId, | ||
| prompt: `# Synthesize Results | ||
| ${work.synthesize} | ||
| ## Work Item Results | ||
| ${results}`, | ||
| capability: PHASE_CAPABILITY[work.phase], | ||
| context: { isolation: "shared", inputs: [] }, | ||
| output: outputSpec(work.phase), | ||
| onComplete, | ||
| instructions: synthesizeInstructions(work.phase, onComplete) | ||
| }; | ||
| } | ||
| function buildGateResponse(state, gate, taskId) { | ||
| if (gate === "clarify") { | ||
| const onComplete = decideCommand(state.workflowId); | ||
| return { type: "gate", gate: "clarify", workflowId: state.workflowId, decisions: state.pendingDecisions, onComplete, instructions: gateClarifyInstructions(onComplete) }; | ||
| } | ||
| if (gate === "plan") { | ||
| const approveCmd = approvePlanCommand(state.workflowId); | ||
| const adjustCmd = adjustPlanCommand(state.workflowId); | ||
| return { type: "gate", gate: "plan", workflowId: state.workflowId, tasks: state.tasks, approach: state.planOutput?.approach, risks: state.planOutput?.risks, onComplete: approveCmd, instructions: gatePlanInstructions(approveCmd, adjustCmd) }; | ||
| } | ||
| const task = taskId ? state.tasks.find((t) => t.id === taskId) : void 0; | ||
| const continueCmd = resolveVerifyCommand(state.workflowId, taskId ?? "", "continue"); | ||
| const stopCmd = resolveVerifyCommand(state.workflowId, taskId ?? "", "stop"); | ||
| return { type: "gate", gate: "verify", workflowId: state.workflowId, taskId, verifyAttempts: task?.verifyAttempts, lastIssues: task?.verifyOutput?.issues, onComplete: continueCmd, instructions: gateVerifyInstructions(task?.title ?? taskId ?? "", task?.verifyAttempts ?? 0, continueCmd, stopCmd) }; | ||
| } | ||
| function buildDoneResponse(state) { | ||
| let message; | ||
| if (state.status === "completed") message = "Workflow completed successfully."; | ||
| else if (state.status === "blocked") message = `Workflow blocked: ${state.blockedReason || "unknown reason"}`; | ||
| else message = `Workflow stopped: ${state.blockedReason || "stopped by user"}`; | ||
| return { type: "done", workflowId: state.workflowId, status: state.status, message, instructions: doneInstructions() }; | ||
| } | ||
| function buildFaultResponse(state, error, issues, recoverable) { | ||
| return { type: "fault", workflowId: state.workflowId, error, issues, recoverable, instructions: faultInstructions(recoverable) }; | ||
| } | ||
| // src/orchestrator.ts | ||
| function clone(value) { | ||
| return JSON.parse(JSON.stringify(value)); | ||
| } | ||
| function now() { | ||
| return (/* @__PURE__ */ new Date()).toISOString(); | ||
| } | ||
| function makeId() { | ||
| return `wf-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; | ||
| } | ||
| function setPhase(state, status, phase) { | ||
| state.status = status; | ||
| state.phase = phase; | ||
| state.updatedAt = now(); | ||
| } | ||
| function findTask(state, taskId) { | ||
| return state.tasks.find((t) => t.id === taskId); | ||
| } | ||
| function readyTasks(state) { | ||
| const doneIds = new Set(state.tasks.filter((t) => t.status === "done").map((t) => t.id)); | ||
| return state.tasks.filter( | ||
| (t) => t.status === "pending" && t.dependsOn.every((dep) => doneIds.has(dep)) | ||
| ); | ||
| } | ||
| function allTasksDone(state) { | ||
| return state.tasks.length > 0 && state.tasks.every((t) => t.status === "done"); | ||
| } | ||
| function createWorkflow(description) { | ||
| const timestamp = now(); | ||
| const state = { | ||
| schemaVersion: "1.0.0", | ||
| workflowId: makeId(), | ||
| description, | ||
| status: "phase_clarify", | ||
| phase: "clarify", | ||
| pendingDecisions: [], | ||
| decisionHistory: [], | ||
| tasks: [], | ||
| escalateAfter: 3, | ||
| createdAt: timestamp, | ||
| updatedAt: timestamp | ||
| }; | ||
| const v = validateWorkflowState(state); | ||
| if (!v.ok) { | ||
| return { state, response: buildFaultResponse(state, "invalid initial state", v.issues, false) }; | ||
| } | ||
| return { state, response: nextResponse(state) }; | ||
| } | ||
| function nextResponse(state) { | ||
| switch (state.status) { | ||
| case "phase_clarify": | ||
| return buildPhaseResponse(state, "clarify"); | ||
| case "gate_clarify": | ||
| return buildGateResponse(state, "clarify"); | ||
| case "phase_plan": | ||
| return buildPhaseResponse(state, "plan"); | ||
| case "gate_plan": | ||
| return buildGateResponse(state, "plan"); | ||
| case "phase_execute": | ||
| case "phase_verify": | ||
| return buildNextTaskResponses(state); | ||
| case "phase_synthesize": | ||
| return buildSynthesizeFromState(state); | ||
| case "gate_verify": | ||
| return buildVerifyGate(state); | ||
| case "completed": | ||
| case "blocked": | ||
| case "stopped": | ||
| return buildDoneResponse(state); | ||
| default: | ||
| return buildFaultResponse(state, `unknown status: ${state.status}`, [], false); | ||
| } | ||
| } | ||
| function buildNextTaskResponses(state) { | ||
| const actionable = []; | ||
| for (const task of state.tasks) { | ||
| if (task.status === "executing") actionable.push({ task, phase: "execute" }); | ||
| else if (task.status === "verifying") actionable.push({ task, phase: "verify" }); | ||
| } | ||
| const ready = readyTasks(state); | ||
| for (const task of ready) { | ||
| task.status = "executing"; | ||
| actionable.push({ task, phase: "execute" }); | ||
| } | ||
| if (actionable.length === 0) { | ||
| if (allTasksDone(state)) { | ||
| state.status = "completed"; | ||
| state.updatedAt = now(); | ||
| return buildDoneResponse(state); | ||
| } | ||
| return buildFaultResponse(state, "no actionable tasks", [], false); | ||
| } | ||
| if (actionable.length === 1) { | ||
| return buildPhaseResponse(state, actionable[0].phase, actionable[0].task.id); | ||
| } | ||
| return buildBatchResponse(state, actionable.map((a) => ({ phase: a.phase, taskId: a.task.id }))); | ||
| } | ||
| function buildVerifyGate(state) { | ||
| const stuck = state.tasks.find((t) => t.status === "verifying" && t.verifyAttempts >= state.escalateAfter); | ||
| if (!stuck) return buildFaultResponse(state, "gate_verify but no stuck task found", [], false); | ||
| return buildGateResponse(state, "verify", stuck.id); | ||
| } | ||
| function buildSynthesizeFromState(state) { | ||
| if (!state.activeWork) return buildFaultResponse(state, "phase_synthesize but no active work", [], false); | ||
| return buildSynthesizeResponse(state, state.activeWork); | ||
| } | ||
| function submitWorkItems(state, phase, input, taskId) { | ||
| const v = validateWorkItems(input); | ||
| if (!v.ok || !v.value) { | ||
| return { response: buildFaultResponse(state, "invalid work items", v.issues, true) }; | ||
| } | ||
| const items = v.value; | ||
| if (items.length === 1 && allItemsHaveResults(items)) { | ||
| return submitPhaseOutput(state, phase, items[0].result, taskId); | ||
| } | ||
| const next = clone(state); | ||
| next.activeWork = { | ||
| phase, | ||
| taskId, | ||
| synthesize: `Combine the results from ${items.length} work items into a single ${phase} output.`, | ||
| items: items.map((item) => ({ | ||
| id: item.id, | ||
| role: item.role, | ||
| task: item.task, | ||
| status: item.result !== void 0 ? "done" : "pending", | ||
| result: item.result !== void 0 ? JSON.stringify(item.result) : void 0 | ||
| })) | ||
| }; | ||
| if (next.activeWork.items.every((i) => i.status === "done")) { | ||
| next.status = "phase_synthesize"; | ||
| next.updatedAt = now(); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| for (const item of next.activeWork.items) { | ||
| if (item.status === "pending") item.status = "running"; | ||
| } | ||
| next.status = "phase_synthesize"; | ||
| next.updatedAt = now(); | ||
| return { state: next, response: buildWorkItemBatchResponse(next, next.activeWork) }; | ||
| } | ||
| function submitWorkItemResult(state, itemId, result) { | ||
| if (!state.activeWork) { | ||
| return { response: buildFaultResponse(state, "no active work items", [], true) }; | ||
| } | ||
| const next = clone(state); | ||
| const item = next.activeWork.items.find((i) => i.id === itemId); | ||
| if (!item) { | ||
| return { response: buildFaultResponse(next, `work item ${itemId} not found`, [], true) }; | ||
| } | ||
| item.status = "done"; | ||
| item.result = result; | ||
| next.updatedAt = now(); | ||
| if (next.activeWork.items.every((i) => i.status === "done")) { | ||
| next.status = "phase_synthesize"; | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| return { state: next, response: buildWorkItemBatchResponse(next, next.activeWork) }; | ||
| } | ||
| function submitSynthesizedResult(state, input) { | ||
| if (!state.activeWork) { | ||
| return { response: buildFaultResponse(state, "no active work to synthesize", [], true) }; | ||
| } | ||
| const work = state.activeWork; | ||
| const next = clone(state); | ||
| next.activeWork = void 0; | ||
| const phaseStatus = `phase_${work.phase}`; | ||
| next.status = phaseStatus; | ||
| next.phase = work.phase; | ||
| return submitPhaseOutput(next, work.phase, input, work.taskId); | ||
| } | ||
| function submitPhaseOutput(state, phase, input, taskId) { | ||
| switch (phase) { | ||
| case "clarify": | ||
| return applyClarify(state, input); | ||
| case "plan": | ||
| return applyPlan(state, input); | ||
| case "execute": | ||
| return applyExecute(state, input, taskId); | ||
| case "verify": | ||
| return applyVerify(state, input, taskId); | ||
| default: | ||
| return { response: buildFaultResponse(state, `unsupported phase: ${phase}`, [], false) }; | ||
| } | ||
| } | ||
| function submitDecisions(state, input) { | ||
| if (state.status !== "gate_clarify") { | ||
| return { response: buildFaultResponse(state, "decisions only valid during gate_clarify", [], true) }; | ||
| } | ||
| const v = validateDecisionAnswers(input); | ||
| if (!v.ok || !v.value) { | ||
| return { response: buildFaultResponse(state, "invalid decision answers", v.issues, true) }; | ||
| } | ||
| const next = clone(state); | ||
| next.decisionHistory.push(...v.value); | ||
| next.pendingDecisions = []; | ||
| setPhase(next, "phase_clarify", "clarify"); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function approvePlan(state) { | ||
| if (state.status !== "gate_plan") { | ||
| return { response: buildFaultResponse(state, "approve-plan only valid during gate_plan", [], true) }; | ||
| } | ||
| const next = clone(state); | ||
| if (next.tasks.length === 0) { | ||
| return { response: buildFaultResponse(next, "no tasks to execute", [], true) }; | ||
| } | ||
| const ready = readyTasks(next); | ||
| for (const task of ready) task.status = "executing"; | ||
| setPhase(next, "phase_execute", "execute"); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function adjustPlan(state, feedback) { | ||
| if (state.status !== "gate_plan") { | ||
| return { response: buildFaultResponse(state, "adjust-plan only valid during gate_plan", [], true) }; | ||
| } | ||
| const next = clone(state); | ||
| next.planOutput = void 0; | ||
| next.tasks = []; | ||
| next.decisionHistory.push({ | ||
| decisionId: "plan-adjustment", | ||
| selectedOptionId: "feedback", | ||
| customInput: feedback | ||
| }); | ||
| setPhase(next, "phase_plan", "plan"); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function resolveVerifyGate(state, taskId, action) { | ||
| if (state.status !== "gate_verify") { | ||
| return { response: buildFaultResponse(state, "resolve-verify only valid during gate_verify", [], true) }; | ||
| } | ||
| const next = clone(state); | ||
| const task = findTask(next, taskId); | ||
| if (!task) { | ||
| return { response: buildFaultResponse(next, `task ${taskId} not found`, [], true) }; | ||
| } | ||
| if (action === "stop") { | ||
| task.status = "failed"; | ||
| next.status = "blocked"; | ||
| next.blockedReason = `User stopped task ${taskId} after ${task.verifyAttempts} verify attempts`; | ||
| next.updatedAt = now(); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| task.status = "executing"; | ||
| setPhase(next, "phase_execute", "execute"); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function stopWorkflow(state, reason = "stopped by user") { | ||
| const next = clone(state); | ||
| next.status = "stopped"; | ||
| next.blockedReason = reason; | ||
| next.updatedAt = now(); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function applyClarify(state, input) { | ||
| const v = validateClarifyOutput(input); | ||
| if (!v.ok || !v.value) { | ||
| return { response: buildFaultResponse(state, "invalid clarify output", v.issues, true) }; | ||
| } | ||
| const next = clone(state); | ||
| next.clarifyOutput = v.value; | ||
| if (!v.value.ready) { | ||
| next.pendingDecisions = v.value.decisions; | ||
| setPhase(next, "gate_clarify", "clarify"); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| next.pendingDecisions = []; | ||
| setPhase(next, "phase_plan", "plan"); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function applyPlan(state, input) { | ||
| const v = validatePlanOutput(input); | ||
| if (!v.ok || !v.value) { | ||
| return { response: buildFaultResponse(state, "invalid plan output", v.issues, true) }; | ||
| } | ||
| const next = clone(state); | ||
| next.planOutput = v.value; | ||
| next.tasks = v.value.tasks.map((td) => ({ | ||
| id: td.id, | ||
| title: td.title, | ||
| scope: Array.isArray(td.scope) ? td.scope : td.scope ? [td.scope] : [], | ||
| context: Array.isArray(td.context) ? td.context : td.context ? [String(td.context)] : [], | ||
| dependsOn: Array.isArray(td.dependsOn) ? td.dependsOn : td.dependsOn ? [td.dependsOn] : [], | ||
| status: "pending", | ||
| verifyAttempts: 0 | ||
| })); | ||
| setPhase(next, "gate_plan", "plan"); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function applyExecute(state, input, taskId) { | ||
| const v = validateExecuteOutput(input); | ||
| if (!v.ok || !v.value) { | ||
| return { response: buildFaultResponse(state, "invalid execute output", v.issues, true) }; | ||
| } | ||
| const next = clone(state); | ||
| const resolvedId = taskId ?? next.tasks.find((t) => t.status === "executing")?.id; | ||
| const task = resolvedId ? findTask(next, resolvedId) : void 0; | ||
| if (!task) { | ||
| return { response: buildFaultResponse(next, "no executing task", [], true) }; | ||
| } | ||
| task.executeOutput = v.value; | ||
| task.status = "verifying"; | ||
| task.verifyAttempts += 1; | ||
| updateWorkflowPhase(next); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function applyVerify(state, input, taskId) { | ||
| const v = validateVerifyOutput(input); | ||
| if (!v.ok || !v.value) { | ||
| return { response: buildFaultResponse(state, "invalid verify output", v.issues, true) }; | ||
| } | ||
| const next = clone(state); | ||
| const resolvedId = taskId ?? next.tasks.find((t) => t.status === "verifying")?.id; | ||
| const task = resolvedId ? findTask(next, resolvedId) : void 0; | ||
| if (!task) { | ||
| return { response: buildFaultResponse(next, "no verifying task", [], true) }; | ||
| } | ||
| task.verifyOutput = v.value; | ||
| if (v.value.passed) { | ||
| task.status = "done"; | ||
| } else if (task.verifyAttempts >= next.escalateAfter) { | ||
| next.status = "gate_verify"; | ||
| next.phase = "verify"; | ||
| next.updatedAt = now(); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } else { | ||
| task.status = "executing"; | ||
| } | ||
| updateWorkflowPhase(next); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function updateWorkflowPhase(state) { | ||
| if (allTasksDone(state)) { | ||
| state.status = "completed"; | ||
| state.updatedAt = now(); | ||
| return; | ||
| } | ||
| const hasExecuting = state.tasks.some((t) => t.status === "executing"); | ||
| const hasVerifying = state.tasks.some((t) => t.status === "verifying"); | ||
| const hasReady = readyTasks(state).length > 0; | ||
| if (hasExecuting || hasReady) { | ||
| setPhase(state, "phase_execute", "execute"); | ||
| } else if (hasVerifying) { | ||
| setPhase(state, "phase_verify", "verify"); | ||
| } | ||
| } | ||
| // src/state-store.ts | ||
| import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs"; | ||
| import { join, dirname } from "path"; | ||
| var STATE_DIR = ".krow/state"; | ||
| function statePath(workflowId, rootDir = process.cwd()) { | ||
| return join(rootDir, STATE_DIR, `${workflowId}.json`); | ||
| } | ||
| function saveState(state, rootDir = process.cwd()) { | ||
| state.updatedAt = (/* @__PURE__ */ new Date()).toISOString(); | ||
| const p = statePath(state.workflowId, rootDir); | ||
| const dir = dirname(p); | ||
| if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); | ||
| writeFileSync(p, JSON.stringify(state, null, 2)); | ||
| } | ||
| function loadState(workflowId, rootDir = process.cwd()) { | ||
| const p = statePath(workflowId, rootDir); | ||
| if (!existsSync(p)) return null; | ||
| return JSON.parse(readFileSync(p, "utf-8")); | ||
| } | ||
| function listStates(rootDir = process.cwd()) { | ||
| const dir = join(rootDir, STATE_DIR); | ||
| if (!existsSync(dir)) return []; | ||
| return readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => { | ||
| try { | ||
| return JSON.parse(readFileSync(join(dir, f), "utf-8")); | ||
| } catch { | ||
| return null; | ||
| } | ||
| }).filter((s) => s !== null).filter((s) => s.status !== "completed" && s.status !== "stopped").sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")); | ||
| } | ||
| export { | ||
| validateWorkItems, | ||
| allItemsHaveResults, | ||
| validateClarifyOutput, | ||
| validatePlanOutput, | ||
| validateExecuteOutput, | ||
| validateVerifyOutput, | ||
| validateDecisionAnswers, | ||
| validateWorkflowState, | ||
| buildClarifyPrompt, | ||
| buildPlanPrompt, | ||
| buildExecutePrompt, | ||
| buildVerifyPrompt, | ||
| buildPhaseResponse, | ||
| buildBatchResponse, | ||
| buildWorkItemBatchResponse, | ||
| buildSynthesizeResponse, | ||
| buildGateResponse, | ||
| buildDoneResponse, | ||
| buildFaultResponse, | ||
| createWorkflow, | ||
| nextResponse, | ||
| submitWorkItems, | ||
| submitWorkItemResult, | ||
| submitSynthesizedResult, | ||
| submitPhaseOutput, | ||
| submitDecisions, | ||
| approvePlan, | ||
| adjustPlan, | ||
| resolveVerifyGate, | ||
| stopWorkflow, | ||
| statePath, | ||
| saveState, | ||
| loadState, | ||
| listStates | ||
| }; |
+1
-1
@@ -16,3 +16,3 @@ #!/usr/bin/env node | ||
| submitWorkItems | ||
| } from "./chunk-MDLCULSL.js"; | ||
| } from "./chunk-6Y7VKUWA.js"; | ||
@@ -19,0 +19,0 @@ // src/cli.ts |
+1
-1
@@ -36,3 +36,3 @@ import { | ||
| validateWorkflowState | ||
| } from "./chunk-MDLCULSL.js"; | ||
| } from "./chunk-6Y7VKUWA.js"; | ||
| export { | ||
@@ -39,0 +39,0 @@ adjustPlan, |
+1
-1
| { | ||
| "name": "krow-cli", | ||
| "version": "0.2.0", | ||
| "version": "0.2.1", | ||
| "description": "A host-agnostic agent harness for coding work", | ||
@@ -5,0 +5,0 @@ "type": "module", |
| // src/validators.ts | ||
| function isObject(v) { | ||
| return typeof v === "object" && v !== null && !Array.isArray(v); | ||
| } | ||
| function isString(v) { | ||
| return typeof v === "string"; | ||
| } | ||
| function isArray(v) { | ||
| return Array.isArray(v); | ||
| } | ||
| function fail(issues) { | ||
| return { ok: false, issues }; | ||
| } | ||
| function pass(value) { | ||
| return { ok: true, issues: [], value }; | ||
| } | ||
| function validateWorkItems(input) { | ||
| if (!isArray(input)) return fail(["work items must be an array"]); | ||
| if (input.length === 0) return fail(["work items must not be empty"]); | ||
| const issues = []; | ||
| for (let i = 0; i < input.length; i++) { | ||
| const item = input[i]; | ||
| if (!isObject(item)) { | ||
| issues.push(`items[${i}] must be an object`); | ||
| continue; | ||
| } | ||
| if (!isString(item.id)) issues.push(`items[${i}].id must be a string`); | ||
| if (!isString(item.role)) issues.push(`items[${i}].role must be a string`); | ||
| if (!isString(item.task)) issues.push(`items[${i}].task must be a string`); | ||
| } | ||
| if (issues.length > 0) return fail(issues); | ||
| return pass(input); | ||
| } | ||
| function allItemsHaveResults(items) { | ||
| return items.every((item) => item.result !== void 0); | ||
| } | ||
| function validateClarifyOutput(input) { | ||
| const issues = []; | ||
| if (!isObject(input)) return fail(["clarify output must be an object"]); | ||
| if (typeof input.ready !== "boolean") issues.push("ready must be a boolean"); | ||
| if (!isString(input.summary) || input.summary.length === 0) issues.push("summary must be a non-empty string"); | ||
| if (!isArray(input.assumptions)) issues.push("assumptions must be an array"); | ||
| if (!isArray(input.decisions)) issues.push("decisions must be an array"); | ||
| if (!input.ready && isArray(input.decisions) && input.decisions.length === 0) { | ||
| issues.push("ready=false requires at least one decision"); | ||
| } | ||
| if (isArray(input.decisions)) { | ||
| for (let i = 0; i < input.decisions.length; i++) { | ||
| const d = input.decisions[i]; | ||
| if (!isObject(d)) { | ||
| issues.push(`decisions[${i}] must be an object`); | ||
| continue; | ||
| } | ||
| if (!isString(d.id)) issues.push(`decisions[${i}].id must be a string`); | ||
| if (!isString(d.question)) issues.push(`decisions[${i}].question must be a string`); | ||
| if (!isArray(d.options) || d.options.length === 0) issues.push(`decisions[${i}].options must be a non-empty array`); | ||
| } | ||
| } | ||
| if (issues.length > 0) return fail(issues); | ||
| return pass(input); | ||
| } | ||
| function validatePlanOutput(input) { | ||
| const issues = []; | ||
| if (!isObject(input)) return fail(["plan output must be an object"]); | ||
| if (!isArray(input.tasks) || input.tasks.length === 0) { | ||
| issues.push("tasks must be a non-empty array"); | ||
| } else { | ||
| for (let i = 0; i < input.tasks.length; i++) { | ||
| const t = input.tasks[i]; | ||
| if (!isObject(t)) { | ||
| issues.push(`tasks[${i}] must be an object`); | ||
| continue; | ||
| } | ||
| if (!isString(t.id)) issues.push(`tasks[${i}].id must be a string`); | ||
| if (!isString(t.title)) issues.push(`tasks[${i}].title must be a string`); | ||
| if (!isArray(t.scope) && !isString(t.scope)) issues.push(`tasks[${i}].scope must be an array or string`); | ||
| } | ||
| } | ||
| if (!isString(input.approach) || input.approach.length === 0) issues.push("approach must be a non-empty string"); | ||
| if (!isArray(input.risks)) issues.push("risks must be an array"); | ||
| if (issues.length > 0) return fail(issues); | ||
| return pass(input); | ||
| } | ||
| function validateExecuteOutput(input) { | ||
| const issues = []; | ||
| if (!isObject(input)) return fail(["execute output must be an object"]); | ||
| if (!isString(input.summary) || input.summary.length === 0) issues.push("summary must be a non-empty string"); | ||
| if (!isArray(input.changedFiles)) issues.push("changedFiles must be an array"); | ||
| if (!isArray(input.notes)) issues.push("notes must be an array"); | ||
| if (issues.length > 0) return fail(issues); | ||
| return pass(input); | ||
| } | ||
| function validateVerifyOutput(input) { | ||
| const issues = []; | ||
| if (!isObject(input)) return fail(["verify output must be an object"]); | ||
| if (typeof input.passed !== "boolean") issues.push("passed must be a boolean"); | ||
| if (!isArray(input.issues)) issues.push("issues must be an array"); | ||
| if (!isString(input.summary) || input.summary.length === 0) issues.push("summary must be a non-empty string"); | ||
| if (isArray(input.issues)) { | ||
| for (let i = 0; i < input.issues.length; i++) { | ||
| const issue = input.issues[i]; | ||
| if (!isObject(issue)) { | ||
| issues.push(`issues[${i}] must be an object`); | ||
| continue; | ||
| } | ||
| if (!isString(issue.severity) || !["error", "warning"].includes(issue.severity)) { | ||
| issues.push(`issues[${i}].severity must be "error" or "warning"`); | ||
| } | ||
| if (!isString(issue.description)) issues.push(`issues[${i}].description must be a string`); | ||
| } | ||
| } | ||
| if (issues.length > 0) return fail(issues); | ||
| return pass(input); | ||
| } | ||
| function validateDecisionAnswers(input) { | ||
| if (!isArray(input)) return fail(["decision answers must be an array"]); | ||
| const issues = []; | ||
| for (let i = 0; i < input.length; i++) { | ||
| const a = input[i]; | ||
| if (!isObject(a)) { | ||
| issues.push(`answers[${i}] must be an object`); | ||
| continue; | ||
| } | ||
| if (!isString(a.decisionId)) issues.push(`answers[${i}].decisionId must be a string`); | ||
| if (!isString(a.selectedOptionId)) issues.push(`answers[${i}].selectedOptionId must be a string`); | ||
| } | ||
| if (issues.length > 0) return fail(issues); | ||
| return pass(input); | ||
| } | ||
| function validateWorkflowState(state) { | ||
| const issues = []; | ||
| if (!isObject(state)) return fail(["state must be an object"]); | ||
| if (!isString(state.schemaVersion)) issues.push("schemaVersion required"); | ||
| if (!isString(state.workflowId)) issues.push("workflowId required"); | ||
| if (!isString(state.description)) issues.push("description required"); | ||
| if (!isString(state.status)) issues.push("status required"); | ||
| if (!isString(state.phase)) issues.push("phase required"); | ||
| if (!isArray(state.pendingDecisions)) issues.push("pendingDecisions must be an array"); | ||
| if (!isArray(state.decisionHistory)) issues.push("decisionHistory must be an array"); | ||
| if (!isArray(state.tasks)) issues.push("tasks must be an array"); | ||
| if (typeof state.escalateAfter !== "number") issues.push("escalateAfter must be a number"); | ||
| if (!isString(state.createdAt)) issues.push("createdAt required"); | ||
| if (!isString(state.updatedAt)) issues.push("updatedAt required"); | ||
| if (issues.length > 0) return fail(issues); | ||
| return pass(state); | ||
| } | ||
| // src/prompts.ts | ||
| function workItemsOutputFormat(phaseOutputExample) { | ||
| return ` | ||
| ## Output Format | ||
| Always return a JSON array of work items: | ||
| \`\`\`json | ||
| [ | ||
| { "id": "string", "role": "string", "task": "string", "result": <phase output or omit> } | ||
| ] | ||
| \`\`\` | ||
| **Simple case** \u2014 you do it yourself, return 1 item with result: | ||
| \`\`\`json | ||
| [{ "id": "main", "role": "main", "task": "did everything", "result": ${phaseOutputExample} }] | ||
| \`\`\` | ||
| **Complex case** \u2014 too big for one agent, return multiple items WITHOUT result: | ||
| \`\`\`json | ||
| [ | ||
| { "id": "s1", "role": "structure-analyst", "task": "analyze project structure and entry points" }, | ||
| { "id": "s2", "role": "dependency-analyst", "task": "analyze internal and external dependencies" } | ||
| ] | ||
| \`\`\` | ||
| These will be executed in parallel by separate agents. Results will be synthesized afterward. | ||
| Choose based on complexity. Most tasks need just 1 item.`; | ||
| } | ||
| function buildClarifyPrompt(state) { | ||
| const parts = []; | ||
| parts.push(`# Clarify Phase`); | ||
| parts.push(``); | ||
| parts.push(`## Goal`); | ||
| parts.push(`User request: "${state.description}"`); | ||
| parts.push(``); | ||
| parts.push(`## Your task`); | ||
| parts.push(`Make this request concrete enough to plan and execute safely.`); | ||
| parts.push(``); | ||
| if (state.decisionHistory.length > 0) { | ||
| parts.push(`## Previous decisions`); | ||
| for (const d of state.decisionHistory) { | ||
| parts.push(`- ${d.decisionId}: ${d.selectedOptionId}${d.customInput ? ` (${d.customInput})` : ""}`); | ||
| } | ||
| parts.push(``); | ||
| } | ||
| parts.push(`## Step 1: Understand`); | ||
| parts.push(`Restate what the user wants in your own words. Identify the core intent.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 2: Assess`); | ||
| parts.push(`Explore the codebase. Find relevant files, patterns, constraints, and dependencies.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 3: Tighten`); | ||
| parts.push(`Identify assumptions you're making. Surface any gaps or ambiguities.`); | ||
| parts.push(`If information is missing and you cannot proceed safely, define decisions for the user.`); | ||
| parts.push(`If everything is clear enough, mark as ready.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 4: Report`); | ||
| parts.push(workItemsOutputFormat(`{ready, summary, assumptions, verifyFocus, decisions}`)); | ||
| return parts.join("\n"); | ||
| } | ||
| function buildPlanPrompt(state) { | ||
| const parts = []; | ||
| parts.push(`# Plan Phase`); | ||
| parts.push(``); | ||
| parts.push(`## Goal`); | ||
| parts.push(`User request: "${state.description}"`); | ||
| parts.push(``); | ||
| if (state.clarifyOutput) { | ||
| parts.push(`## Clarify summary`); | ||
| parts.push(state.clarifyOutput.summary); | ||
| parts.push(``); | ||
| if (state.clarifyOutput.assumptions.length > 0) { | ||
| parts.push(`## Assumptions`); | ||
| for (const a of state.clarifyOutput.assumptions) { | ||
| parts.push(`- ${a}`); | ||
| } | ||
| parts.push(``); | ||
| } | ||
| } | ||
| const adjustments = state.decisionHistory.filter((d) => d.decisionId === "plan-adjustment"); | ||
| if (adjustments.length > 0) { | ||
| parts.push(`## Plan adjustment feedback`); | ||
| for (const a of adjustments) { | ||
| parts.push(`- ${a.customInput}`); | ||
| } | ||
| parts.push(``); | ||
| } | ||
| parts.push(`## Your task`); | ||
| parts.push(`Design an approach and break it into concrete, ordered tasks.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 1: Understand`); | ||
| parts.push(`Review the clarify output and assumptions. Understand what needs to happen.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 2: Assess`); | ||
| parts.push(`Identify files, modules, and dependencies involved. Check feasibility.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 3: Design`); | ||
| parts.push(`Break the work into bounded tasks. Each task should:`); | ||
| parts.push(`- Have a clear, narrow scope (specific files/modules)`); | ||
| parts.push(`- Be independently verifiable`); | ||
| parts.push(`- List what context it needs from earlier tasks`); | ||
| parts.push(`- Declare dependencies on other tasks`); | ||
| parts.push(``); | ||
| parts.push(`## Step 4: Report`); | ||
| parts.push(workItemsOutputFormat(`{approach, risks, tasks: [{id, title, scope, context, dependsOn}]}`)); | ||
| return parts.join("\n"); | ||
| } | ||
| function buildExecutePrompt(state, task) { | ||
| const parts = []; | ||
| parts.push(`# Execute Phase \u2014 Task: ${task.title}`); | ||
| parts.push(``); | ||
| parts.push(`## Goal`); | ||
| parts.push(`User request: "${state.description}"`); | ||
| parts.push(``); | ||
| parts.push(`## Task scope`); | ||
| parts.push(`Files/modules to modify: ${task.scope.join(", ") || "as needed"}`); | ||
| parts.push(``); | ||
| parts.push(`## Task context`); | ||
| const ctx = Array.isArray(task.context) ? task.context : task.context ? [task.context] : []; | ||
| parts.push(`Files/data to read: ${ctx.join(", ") || "as needed"}`); | ||
| parts.push(``); | ||
| if (state.clarifyOutput) { | ||
| parts.push(`## Clarify summary`); | ||
| parts.push(state.clarifyOutput.summary); | ||
| parts.push(``); | ||
| } | ||
| if (state.planOutput) { | ||
| parts.push(`## Approach`); | ||
| parts.push(state.planOutput.approach); | ||
| parts.push(``); | ||
| } | ||
| if (task.verifyOutput && !task.verifyOutput.passed) { | ||
| parts.push(`## Previous verification feedback`); | ||
| parts.push(task.verifyOutput.summary); | ||
| if (task.verifyOutput.retryHint) { | ||
| parts.push(`Retry hint: ${task.verifyOutput.retryHint}`); | ||
| } | ||
| for (const issue of task.verifyOutput.issues) { | ||
| parts.push(`- [${issue.severity}] ${issue.description}${issue.suggestion ? ` \u2192 ${issue.suggestion}` : ""}`); | ||
| } | ||
| parts.push(``); | ||
| } | ||
| parts.push(`## Your task`); | ||
| parts.push(`Implement the changes described in this task.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 1: Understand`); | ||
| parts.push(`Review what this task must produce. Check the scope and constraints.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 2: Assess`); | ||
| parts.push(`Read the target files. Understand the current state before making changes.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 3: Implement`); | ||
| parts.push(`Make the changes. Stay within scope.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 4: Report`); | ||
| parts.push(workItemsOutputFormat(`{summary, changedFiles, notes}`)); | ||
| return parts.join("\n"); | ||
| } | ||
| function buildVerifyPrompt(state, task) { | ||
| const parts = []; | ||
| parts.push(`# Verify Phase \u2014 Task: ${task.title}`); | ||
| parts.push(``); | ||
| parts.push(`## Goal`); | ||
| parts.push(`Determine if the task was completed correctly.`); | ||
| parts.push(``); | ||
| if (task.executeOutput) { | ||
| parts.push(`## What was done`); | ||
| parts.push(task.executeOutput.summary); | ||
| parts.push(``); | ||
| parts.push(`## Changed files`); | ||
| for (const f of task.executeOutput.changedFiles) { | ||
| parts.push(`- ${f}`); | ||
| } | ||
| parts.push(``); | ||
| if (task.executeOutput.notes.length > 0) { | ||
| parts.push(`## Notes from executor`); | ||
| for (const n of task.executeOutput.notes) { | ||
| parts.push(`- ${n}`); | ||
| } | ||
| parts.push(``); | ||
| } | ||
| } | ||
| if (state.clarifyOutput?.verifyFocus && state.clarifyOutput.verifyFocus.length > 0) { | ||
| parts.push(`## Verify focus`); | ||
| for (const f of state.clarifyOutput.verifyFocus) { | ||
| parts.push(`- ${f}`); | ||
| } | ||
| parts.push(``); | ||
| } | ||
| parts.push(`## Attempt ${task.verifyAttempts}`); | ||
| parts.push(``); | ||
| parts.push(`## Your task`); | ||
| parts.push(`Try to disprove the claimed result. Report what you find.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 1: Understand`); | ||
| parts.push(`Review what was claimed to be done.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 2: Assess`); | ||
| parts.push(`Read the changed files. Run relevant checks or tests if applicable.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 3: Validate`); | ||
| parts.push(`Check each claim against evidence. Look for missing pieces, bugs, or regressions.`); | ||
| parts.push(``); | ||
| parts.push(`## Step 4: Report`); | ||
| parts.push(workItemsOutputFormat(`{passed, issues: [{severity, description, suggestion?}], summary, retryHint?}`)); | ||
| return parts.join("\n"); | ||
| } | ||
| // src/instructions.ts | ||
| var _prefix; | ||
| function cliPrefix() { | ||
| if (_prefix) return _prefix; | ||
| const arg1 = process.argv[1] ?? ""; | ||
| if (arg1.includes("cli.ts") || arg1.includes("cli.js")) { | ||
| _prefix = `node ${arg1}`; | ||
| } else { | ||
| _prefix = "krow"; | ||
| } | ||
| return _prefix; | ||
| } | ||
| function submitWorkItemsCommand(workflowId, phase, taskId) { | ||
| const taskArg = taskId ? ` --task ${taskId}` : ""; | ||
| return `${cliPrefix()} submit ${workflowId} ${phase}${taskArg} '<JSON>'`; | ||
| } | ||
| function submitWorkItemResultCommand(workflowId, itemId) { | ||
| return `${cliPrefix()} submit-item ${workflowId} ${itemId} '<RESULT>'`; | ||
| } | ||
| function submitSynthesizedCommand(workflowId) { | ||
| return `${cliPrefix()} submit-synthesized ${workflowId} '<JSON>'`; | ||
| } | ||
| function decideCommand(workflowId) { | ||
| return `${cliPrefix()} decide ${workflowId} '<JSON>'`; | ||
| } | ||
| function approvePlanCommand(workflowId) { | ||
| return `${cliPrefix()} approve-plan ${workflowId}`; | ||
| } | ||
| function adjustPlanCommand(workflowId) { | ||
| return `${cliPrefix()} adjust-plan ${workflowId} '<feedback>'`; | ||
| } | ||
| function resolveVerifyCommand(workflowId, taskId, action) { | ||
| return `${cliPrefix()} resolve-verify ${workflowId} ${taskId} ${action}`; | ||
| } | ||
| function phaseInstructions(phase, onComplete) { | ||
| return [ | ||
| `Execute the prompt for the "${phase}" phase.`, | ||
| `Return your output as a JSON array of work items: [{id, role, task, result?}, ...]`, | ||
| `If you can do it all yourself, return 1 item with result included.`, | ||
| `If complex, return multiple items (without result) \u2014 they'll be executed in parallel.`, | ||
| `Then run: ${onComplete}` | ||
| ].join("\n"); | ||
| } | ||
| function gateClarifyInstructions(onComplete) { | ||
| return [ | ||
| `Present each decision to the user and collect their answers.`, | ||
| `Then run: ${onComplete}`, | ||
| `Replace '<JSON>' with an array of { decisionId, selectedOptionId, customInput? }.` | ||
| ].join("\n"); | ||
| } | ||
| function gatePlanInstructions(approveCmd, adjustCmd) { | ||
| return [ | ||
| `Present the plan to the user for review.`, | ||
| `If approved, run: ${approveCmd}`, | ||
| `If adjustments needed, run: ${adjustCmd}` | ||
| ].join("\n"); | ||
| } | ||
| function gateVerifyInstructions(taskTitle, attempts, continueCmd, stopCmd) { | ||
| return [ | ||
| `Task "${taskTitle}" has failed verification ${attempts} time(s).`, | ||
| `Present the issues to the user.`, | ||
| `Keep trying: ${continueCmd}`, | ||
| `Stop this task: ${stopCmd}` | ||
| ].join("\n"); | ||
| } | ||
| function batchInstructions(count) { | ||
| return [ | ||
| `${count} items to execute in parallel.`, | ||
| `Execute each independently. Submit each result separately using its onComplete command.` | ||
| ].join("\n"); | ||
| } | ||
| function synthesizeInstructions(phase, onComplete) { | ||
| return [ | ||
| `All work items completed. Synthesize the results into a single ${phase} output.`, | ||
| `Then run: ${onComplete}` | ||
| ].join("\n"); | ||
| } | ||
| function doneInstructions() { | ||
| return "The workflow has reached a terminal state. Report the result to the user."; | ||
| } | ||
| function faultInstructions(recoverable) { | ||
| if (recoverable) return "A recoverable error occurred. Review the issues and retry."; | ||
| return "An unrecoverable error occurred. Report the error to the user."; | ||
| } | ||
| // src/responses.ts | ||
| var PHASE_CAPABILITY = { | ||
| clarify: "read", | ||
| plan: "read", | ||
| execute: "write", | ||
| verify: "read" | ||
| }; | ||
| function clarifyContext() { | ||
| return { isolation: "isolated", inputs: [] }; | ||
| } | ||
| function planContext(state) { | ||
| const inputs = []; | ||
| if (state.clarifyOutput) inputs.push({ type: "data", ref: "clarifyOutput" }); | ||
| return { isolation: "isolated", inputs }; | ||
| } | ||
| function executeContext(state, task) { | ||
| const inputs = []; | ||
| if (state.clarifyOutput) inputs.push({ type: "data", ref: "clarifyOutput" }); | ||
| if (state.planOutput) inputs.push({ type: "data", ref: "planOutput.approach" }); | ||
| const ctx = Array.isArray(task.context) ? task.context : task.context ? [task.context] : []; | ||
| for (const c of ctx) inputs.push({ type: "file", ref: c }); | ||
| return { isolation: "isolated", inputs }; | ||
| } | ||
| function verifyContext(state, task) { | ||
| const inputs = []; | ||
| if (task.executeOutput) { | ||
| inputs.push({ type: "data", ref: `tasks.${task.id}.executeOutput` }); | ||
| for (const f of task.executeOutput.changedFiles) inputs.push({ type: "file", ref: f }); | ||
| } | ||
| if (state.clarifyOutput?.verifyFocus) inputs.push({ type: "data", ref: "clarifyOutput.verifyFocus" }); | ||
| return { isolation: "isolated", inputs }; | ||
| } | ||
| function outputSpec(phase) { | ||
| return { schema: `${phase}-output`, format: phase === "execute" ? "files" : "json" }; | ||
| } | ||
| function buildPromptForTask(state, phase, task) { | ||
| switch (phase) { | ||
| case "execute": | ||
| return buildExecutePrompt(state, task); | ||
| case "verify": | ||
| return buildVerifyPrompt(state, task); | ||
| default: | ||
| return ""; | ||
| } | ||
| } | ||
| function contextForTask(state, phase, task) { | ||
| switch (phase) { | ||
| case "execute": | ||
| return executeContext(state, task); | ||
| case "verify": | ||
| return verifyContext(state, task); | ||
| default: | ||
| return { isolation: "isolated", inputs: [] }; | ||
| } | ||
| } | ||
| function buildPhaseResponse(state, phase, taskId) { | ||
| if (phase === "clarify" || phase === "plan") { | ||
| const onComplete2 = submitWorkItemsCommand(state.workflowId, phase); | ||
| return { | ||
| type: "phase", | ||
| phase, | ||
| workflowId: state.workflowId, | ||
| prompt: phase === "clarify" ? buildClarifyPrompt(state) : buildPlanPrompt(state), | ||
| capability: PHASE_CAPABILITY[phase], | ||
| context: phase === "clarify" ? clarifyContext() : planContext(state), | ||
| output: outputSpec(phase), | ||
| onComplete: onComplete2, | ||
| instructions: phaseInstructions(phase, onComplete2) | ||
| }; | ||
| } | ||
| const task = taskId ? state.tasks.find((t) => t.id === taskId) : void 0; | ||
| const onComplete = submitWorkItemsCommand(state.workflowId, phase, taskId); | ||
| if (!task) { | ||
| return { | ||
| type: "phase", | ||
| phase, | ||
| workflowId: state.workflowId, | ||
| prompt: `Error: no task found for ${phase} phase.`, | ||
| capability: PHASE_CAPABILITY[phase], | ||
| context: { isolation: "isolated", inputs: [] }, | ||
| output: outputSpec(phase), | ||
| onComplete, | ||
| instructions: phaseInstructions(phase, onComplete) | ||
| }; | ||
| } | ||
| return { | ||
| type: "phase", | ||
| phase, | ||
| workflowId: state.workflowId, | ||
| taskId: task.id, | ||
| prompt: buildPromptForTask(state, phase, task), | ||
| capability: PHASE_CAPABILITY[phase], | ||
| context: contextForTask(state, phase, task), | ||
| output: outputSpec(phase), | ||
| onComplete, | ||
| instructions: phaseInstructions(phase, onComplete) | ||
| }; | ||
| } | ||
| function buildBatchResponse(state, items) { | ||
| const responses = items.map((item) => buildPhaseResponse(state, item.phase, item.taskId)); | ||
| return { | ||
| type: "batch", | ||
| workflowId: state.workflowId, | ||
| responses, | ||
| instructions: batchInstructions(responses.length) | ||
| }; | ||
| } | ||
| function buildWorkItemBatchResponse(state, work) { | ||
| const running = work.items.filter((i) => i.status === "running" || i.status === "pending"); | ||
| const responses = running.map((item) => { | ||
| const onComplete = submitWorkItemResultCommand(state.workflowId, item.id); | ||
| return { | ||
| type: "phase", | ||
| phase: work.phase, | ||
| workflowId: state.workflowId, | ||
| taskId: work.taskId, | ||
| prompt: `# Work Item: ${item.role} | ||
| ${item.task}`, | ||
| capability: PHASE_CAPABILITY[work.phase], | ||
| context: { isolation: "isolated", inputs: [] }, | ||
| output: { schema: "work-item-result", format: "json" }, | ||
| onComplete, | ||
| instructions: `Execute this work item. Return your result. Then run: ${onComplete}` | ||
| }; | ||
| }); | ||
| return { | ||
| type: "batch", | ||
| workflowId: state.workflowId, | ||
| responses, | ||
| instructions: batchInstructions(responses.length) | ||
| }; | ||
| } | ||
| function buildSynthesizeResponse(state, work) { | ||
| const results = work.items.map((i) => `### ${i.role} | ||
| ${i.result ?? "(no result)"}`).join("\n\n"); | ||
| const onComplete = submitSynthesizedCommand(state.workflowId); | ||
| return { | ||
| type: "phase", | ||
| phase: work.phase, | ||
| workflowId: state.workflowId, | ||
| taskId: work.taskId, | ||
| prompt: `# Synthesize Results | ||
| ${work.synthesize} | ||
| ## Work Item Results | ||
| ${results}`, | ||
| capability: PHASE_CAPABILITY[work.phase], | ||
| context: { isolation: "shared", inputs: [] }, | ||
| output: outputSpec(work.phase), | ||
| onComplete, | ||
| instructions: synthesizeInstructions(work.phase, onComplete) | ||
| }; | ||
| } | ||
| function buildGateResponse(state, gate, taskId) { | ||
| if (gate === "clarify") { | ||
| const onComplete = decideCommand(state.workflowId); | ||
| return { type: "gate", gate: "clarify", workflowId: state.workflowId, decisions: state.pendingDecisions, onComplete, instructions: gateClarifyInstructions(onComplete) }; | ||
| } | ||
| if (gate === "plan") { | ||
| const approveCmd = approvePlanCommand(state.workflowId); | ||
| const adjustCmd = adjustPlanCommand(state.workflowId); | ||
| return { type: "gate", gate: "plan", workflowId: state.workflowId, tasks: state.tasks, approach: state.planOutput?.approach, risks: state.planOutput?.risks, onComplete: approveCmd, instructions: gatePlanInstructions(approveCmd, adjustCmd) }; | ||
| } | ||
| const task = taskId ? state.tasks.find((t) => t.id === taskId) : void 0; | ||
| const continueCmd = resolveVerifyCommand(state.workflowId, taskId ?? "", "continue"); | ||
| const stopCmd = resolveVerifyCommand(state.workflowId, taskId ?? "", "stop"); | ||
| return { type: "gate", gate: "verify", workflowId: state.workflowId, taskId, verifyAttempts: task?.verifyAttempts, lastIssues: task?.verifyOutput?.issues, onComplete: continueCmd, instructions: gateVerifyInstructions(task?.title ?? taskId ?? "", task?.verifyAttempts ?? 0, continueCmd, stopCmd) }; | ||
| } | ||
| function buildDoneResponse(state) { | ||
| let message; | ||
| if (state.status === "completed") message = "Workflow completed successfully."; | ||
| else if (state.status === "blocked") message = `Workflow blocked: ${state.blockedReason || "unknown reason"}`; | ||
| else message = `Workflow stopped: ${state.blockedReason || "stopped by user"}`; | ||
| return { type: "done", workflowId: state.workflowId, status: state.status, message, instructions: doneInstructions() }; | ||
| } | ||
| function buildFaultResponse(state, error, issues, recoverable) { | ||
| return { type: "fault", workflowId: state.workflowId, error, issues, recoverable, instructions: faultInstructions(recoverable) }; | ||
| } | ||
| // src/orchestrator.ts | ||
| function clone(value) { | ||
| return JSON.parse(JSON.stringify(value)); | ||
| } | ||
| function now() { | ||
| return (/* @__PURE__ */ new Date()).toISOString(); | ||
| } | ||
| function makeId() { | ||
| return `wf-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; | ||
| } | ||
| function setPhase(state, status, phase) { | ||
| state.status = status; | ||
| state.phase = phase; | ||
| state.updatedAt = now(); | ||
| } | ||
| function findTask(state, taskId) { | ||
| return state.tasks.find((t) => t.id === taskId); | ||
| } | ||
| function readyTasks(state) { | ||
| const doneIds = new Set(state.tasks.filter((t) => t.status === "done").map((t) => t.id)); | ||
| return state.tasks.filter( | ||
| (t) => t.status === "pending" && t.dependsOn.every((dep) => doneIds.has(dep)) | ||
| ); | ||
| } | ||
| function allTasksDone(state) { | ||
| return state.tasks.length > 0 && state.tasks.every((t) => t.status === "done"); | ||
| } | ||
| function createWorkflow(description) { | ||
| const timestamp = now(); | ||
| const state = { | ||
| schemaVersion: "1.0.0", | ||
| workflowId: makeId(), | ||
| description, | ||
| status: "phase_clarify", | ||
| phase: "clarify", | ||
| pendingDecisions: [], | ||
| decisionHistory: [], | ||
| tasks: [], | ||
| escalateAfter: 3, | ||
| createdAt: timestamp, | ||
| updatedAt: timestamp | ||
| }; | ||
| const v = validateWorkflowState(state); | ||
| if (!v.ok) { | ||
| return { state, response: buildFaultResponse(state, "invalid initial state", v.issues, false) }; | ||
| } | ||
| return { state, response: nextResponse(state) }; | ||
| } | ||
| function nextResponse(state) { | ||
| switch (state.status) { | ||
| case "phase_clarify": | ||
| return buildPhaseResponse(state, "clarify"); | ||
| case "gate_clarify": | ||
| return buildGateResponse(state, "clarify"); | ||
| case "phase_plan": | ||
| return buildPhaseResponse(state, "plan"); | ||
| case "gate_plan": | ||
| return buildGateResponse(state, "plan"); | ||
| case "phase_execute": | ||
| case "phase_verify": | ||
| return buildNextTaskResponses(state); | ||
| case "phase_synthesize": | ||
| return buildSynthesizeFromState(state); | ||
| case "gate_verify": | ||
| return buildVerifyGate(state); | ||
| case "completed": | ||
| case "blocked": | ||
| case "stopped": | ||
| return buildDoneResponse(state); | ||
| default: | ||
| return buildFaultResponse(state, `unknown status: ${state.status}`, [], false); | ||
| } | ||
| } | ||
| function buildNextTaskResponses(state) { | ||
| const actionable = []; | ||
| for (const task of state.tasks) { | ||
| if (task.status === "executing") actionable.push({ task, phase: "execute" }); | ||
| else if (task.status === "verifying") actionable.push({ task, phase: "verify" }); | ||
| } | ||
| const ready = readyTasks(state); | ||
| for (const task of ready) { | ||
| task.status = "executing"; | ||
| actionable.push({ task, phase: "execute" }); | ||
| } | ||
| if (actionable.length === 0) { | ||
| if (allTasksDone(state)) { | ||
| state.status = "completed"; | ||
| state.updatedAt = now(); | ||
| return buildDoneResponse(state); | ||
| } | ||
| return buildFaultResponse(state, "no actionable tasks", [], false); | ||
| } | ||
| if (actionable.length === 1) { | ||
| return buildPhaseResponse(state, actionable[0].phase, actionable[0].task.id); | ||
| } | ||
| return buildBatchResponse(state, actionable.map((a) => ({ phase: a.phase, taskId: a.task.id }))); | ||
| } | ||
| function buildVerifyGate(state) { | ||
| const stuck = state.tasks.find((t) => t.status === "verifying" && t.verifyAttempts >= state.escalateAfter); | ||
| if (!stuck) return buildFaultResponse(state, "gate_verify but no stuck task found", [], false); | ||
| return buildGateResponse(state, "verify", stuck.id); | ||
| } | ||
| function buildSynthesizeFromState(state) { | ||
| if (!state.activeWork) return buildFaultResponse(state, "phase_synthesize but no active work", [], false); | ||
| return buildSynthesizeResponse(state, state.activeWork); | ||
| } | ||
| function submitWorkItems(state, phase, input, taskId) { | ||
| const v = validateWorkItems(input); | ||
| if (!v.ok || !v.value) { | ||
| return { response: buildFaultResponse(state, "invalid work items", v.issues, true) }; | ||
| } | ||
| const items = v.value; | ||
| if (items.length === 1 && allItemsHaveResults(items)) { | ||
| return submitPhaseOutput(state, phase, items[0].result, taskId); | ||
| } | ||
| const next = clone(state); | ||
| next.activeWork = { | ||
| phase, | ||
| taskId, | ||
| synthesize: `Combine the results from ${items.length} work items into a single ${phase} output.`, | ||
| items: items.map((item) => ({ | ||
| id: item.id, | ||
| role: item.role, | ||
| task: item.task, | ||
| status: item.result !== void 0 ? "done" : "pending", | ||
| result: item.result !== void 0 ? JSON.stringify(item.result) : void 0 | ||
| })) | ||
| }; | ||
| if (next.activeWork.items.every((i) => i.status === "done")) { | ||
| next.status = "phase_synthesize"; | ||
| next.updatedAt = now(); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| for (const item of next.activeWork.items) { | ||
| if (item.status === "pending") item.status = "running"; | ||
| } | ||
| next.status = "phase_synthesize"; | ||
| next.updatedAt = now(); | ||
| return { state: next, response: buildWorkItemBatchResponse(next, next.activeWork) }; | ||
| } | ||
| function submitWorkItemResult(state, itemId, result) { | ||
| if (!state.activeWork) { | ||
| return { response: buildFaultResponse(state, "no active work items", [], true) }; | ||
| } | ||
| const next = clone(state); | ||
| const item = next.activeWork.items.find((i) => i.id === itemId); | ||
| if (!item) { | ||
| return { response: buildFaultResponse(next, `work item ${itemId} not found`, [], true) }; | ||
| } | ||
| item.status = "done"; | ||
| item.result = result; | ||
| next.updatedAt = now(); | ||
| if (next.activeWork.items.every((i) => i.status === "done")) { | ||
| next.status = "phase_synthesize"; | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| return { state: next, response: buildWorkItemBatchResponse(next, next.activeWork) }; | ||
| } | ||
| function submitSynthesizedResult(state, input) { | ||
| if (!state.activeWork) { | ||
| return { response: buildFaultResponse(state, "no active work to synthesize", [], true) }; | ||
| } | ||
| const work = state.activeWork; | ||
| const next = clone(state); | ||
| next.activeWork = void 0; | ||
| const phaseStatus = `phase_${work.phase}`; | ||
| next.status = phaseStatus; | ||
| next.phase = work.phase; | ||
| return submitPhaseOutput(next, work.phase, input, work.taskId); | ||
| } | ||
| function submitPhaseOutput(state, phase, input, taskId) { | ||
| switch (phase) { | ||
| case "clarify": | ||
| return applyClarify(state, input); | ||
| case "plan": | ||
| return applyPlan(state, input); | ||
| case "execute": | ||
| return applyExecute(state, input, taskId); | ||
| case "verify": | ||
| return applyVerify(state, input, taskId); | ||
| default: | ||
| return { response: buildFaultResponse(state, `unsupported phase: ${phase}`, [], false) }; | ||
| } | ||
| } | ||
| function submitDecisions(state, input) { | ||
| if (state.status !== "gate_clarify") { | ||
| return { response: buildFaultResponse(state, "decisions only valid during gate_clarify", [], true) }; | ||
| } | ||
| const v = validateDecisionAnswers(input); | ||
| if (!v.ok || !v.value) { | ||
| return { response: buildFaultResponse(state, "invalid decision answers", v.issues, true) }; | ||
| } | ||
| const next = clone(state); | ||
| next.decisionHistory.push(...v.value); | ||
| next.pendingDecisions = []; | ||
| setPhase(next, "phase_clarify", "clarify"); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function approvePlan(state) { | ||
| if (state.status !== "gate_plan") { | ||
| return { response: buildFaultResponse(state, "approve-plan only valid during gate_plan", [], true) }; | ||
| } | ||
| const next = clone(state); | ||
| if (next.tasks.length === 0) { | ||
| return { response: buildFaultResponse(next, "no tasks to execute", [], true) }; | ||
| } | ||
| const ready = readyTasks(next); | ||
| for (const task of ready) task.status = "executing"; | ||
| setPhase(next, "phase_execute", "execute"); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function adjustPlan(state, feedback) { | ||
| if (state.status !== "gate_plan") { | ||
| return { response: buildFaultResponse(state, "adjust-plan only valid during gate_plan", [], true) }; | ||
| } | ||
| const next = clone(state); | ||
| next.planOutput = void 0; | ||
| next.tasks = []; | ||
| next.decisionHistory.push({ | ||
| decisionId: "plan-adjustment", | ||
| selectedOptionId: "feedback", | ||
| customInput: feedback | ||
| }); | ||
| setPhase(next, "phase_plan", "plan"); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function resolveVerifyGate(state, taskId, action) { | ||
| if (state.status !== "gate_verify") { | ||
| return { response: buildFaultResponse(state, "resolve-verify only valid during gate_verify", [], true) }; | ||
| } | ||
| const next = clone(state); | ||
| const task = findTask(next, taskId); | ||
| if (!task) { | ||
| return { response: buildFaultResponse(next, `task ${taskId} not found`, [], true) }; | ||
| } | ||
| if (action === "stop") { | ||
| task.status = "failed"; | ||
| next.status = "blocked"; | ||
| next.blockedReason = `User stopped task ${taskId} after ${task.verifyAttempts} verify attempts`; | ||
| next.updatedAt = now(); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| task.status = "executing"; | ||
| setPhase(next, "phase_execute", "execute"); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function stopWorkflow(state, reason = "stopped by user") { | ||
| const next = clone(state); | ||
| next.status = "stopped"; | ||
| next.blockedReason = reason; | ||
| next.updatedAt = now(); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function applyClarify(state, input) { | ||
| const v = validateClarifyOutput(input); | ||
| if (!v.ok || !v.value) { | ||
| return { response: buildFaultResponse(state, "invalid clarify output", v.issues, true) }; | ||
| } | ||
| const next = clone(state); | ||
| next.clarifyOutput = v.value; | ||
| if (!v.value.ready) { | ||
| next.pendingDecisions = v.value.decisions; | ||
| setPhase(next, "gate_clarify", "clarify"); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| next.pendingDecisions = []; | ||
| setPhase(next, "phase_plan", "plan"); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function applyPlan(state, input) { | ||
| const v = validatePlanOutput(input); | ||
| if (!v.ok || !v.value) { | ||
| return { response: buildFaultResponse(state, "invalid plan output", v.issues, true) }; | ||
| } | ||
| const next = clone(state); | ||
| next.planOutput = v.value; | ||
| next.tasks = v.value.tasks.map((td) => ({ | ||
| id: td.id, | ||
| title: td.title, | ||
| scope: Array.isArray(td.scope) ? td.scope : td.scope ? [td.scope] : [], | ||
| context: Array.isArray(td.context) ? td.context : td.context ? [String(td.context)] : [], | ||
| dependsOn: Array.isArray(td.dependsOn) ? td.dependsOn : td.dependsOn ? [td.dependsOn] : [], | ||
| status: "pending", | ||
| verifyAttempts: 0 | ||
| })); | ||
| setPhase(next, "gate_plan", "plan"); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function applyExecute(state, input, taskId) { | ||
| const v = validateExecuteOutput(input); | ||
| if (!v.ok || !v.value) { | ||
| return { response: buildFaultResponse(state, "invalid execute output", v.issues, true) }; | ||
| } | ||
| const next = clone(state); | ||
| const resolvedId = taskId ?? next.tasks.find((t) => t.status === "executing")?.id; | ||
| const task = resolvedId ? findTask(next, resolvedId) : void 0; | ||
| if (!task) { | ||
| return { response: buildFaultResponse(next, "no executing task", [], true) }; | ||
| } | ||
| task.executeOutput = v.value; | ||
| task.status = "verifying"; | ||
| task.verifyAttempts += 1; | ||
| updateWorkflowPhase(next); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function applyVerify(state, input, taskId) { | ||
| const v = validateVerifyOutput(input); | ||
| if (!v.ok || !v.value) { | ||
| return { response: buildFaultResponse(state, "invalid verify output", v.issues, true) }; | ||
| } | ||
| const next = clone(state); | ||
| const resolvedId = taskId ?? next.tasks.find((t) => t.status === "verifying")?.id; | ||
| const task = resolvedId ? findTask(next, resolvedId) : void 0; | ||
| if (!task) { | ||
| return { response: buildFaultResponse(next, "no verifying task", [], true) }; | ||
| } | ||
| task.verifyOutput = v.value; | ||
| if (v.value.passed) { | ||
| task.status = "done"; | ||
| } else if (task.verifyAttempts >= next.escalateAfter) { | ||
| next.status = "gate_verify"; | ||
| next.phase = "verify"; | ||
| next.updatedAt = now(); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } else { | ||
| task.status = "executing"; | ||
| } | ||
| updateWorkflowPhase(next); | ||
| return { state: next, response: nextResponse(next) }; | ||
| } | ||
| function updateWorkflowPhase(state) { | ||
| if (allTasksDone(state)) { | ||
| state.status = "completed"; | ||
| state.updatedAt = now(); | ||
| return; | ||
| } | ||
| const hasExecuting = state.tasks.some((t) => t.status === "executing"); | ||
| const hasVerifying = state.tasks.some((t) => t.status === "verifying"); | ||
| const hasReady = readyTasks(state).length > 0; | ||
| if (hasExecuting || hasReady) { | ||
| setPhase(state, "phase_execute", "execute"); | ||
| } else if (hasVerifying) { | ||
| setPhase(state, "phase_verify", "verify"); | ||
| } | ||
| } | ||
| // src/state-store.ts | ||
| import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs"; | ||
| import { join, dirname } from "path"; | ||
| var STATE_DIR = ".krow/state"; | ||
| function statePath(workflowId, rootDir = process.cwd()) { | ||
| return join(rootDir, STATE_DIR, `${workflowId}.json`); | ||
| } | ||
| function saveState(state, rootDir = process.cwd()) { | ||
| state.updatedAt = (/* @__PURE__ */ new Date()).toISOString(); | ||
| const p = statePath(state.workflowId, rootDir); | ||
| const dir = dirname(p); | ||
| if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); | ||
| writeFileSync(p, JSON.stringify(state, null, 2)); | ||
| } | ||
| function loadState(workflowId, rootDir = process.cwd()) { | ||
| const p = statePath(workflowId, rootDir); | ||
| if (!existsSync(p)) return null; | ||
| return JSON.parse(readFileSync(p, "utf-8")); | ||
| } | ||
| function listStates(rootDir = process.cwd()) { | ||
| const dir = join(rootDir, STATE_DIR); | ||
| if (!existsSync(dir)) return []; | ||
| return readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => { | ||
| try { | ||
| return JSON.parse(readFileSync(join(dir, f), "utf-8")); | ||
| } catch { | ||
| return null; | ||
| } | ||
| }).filter((s) => s !== null).filter((s) => s.status !== "completed" && s.status !== "stopped").sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")); | ||
| } | ||
| export { | ||
| validateWorkItems, | ||
| allItemsHaveResults, | ||
| validateClarifyOutput, | ||
| validatePlanOutput, | ||
| validateExecuteOutput, | ||
| validateVerifyOutput, | ||
| validateDecisionAnswers, | ||
| validateWorkflowState, | ||
| buildClarifyPrompt, | ||
| buildPlanPrompt, | ||
| buildExecutePrompt, | ||
| buildVerifyPrompt, | ||
| buildPhaseResponse, | ||
| buildBatchResponse, | ||
| buildWorkItemBatchResponse, | ||
| buildSynthesizeResponse, | ||
| buildGateResponse, | ||
| buildDoneResponse, | ||
| buildFaultResponse, | ||
| createWorkflow, | ||
| nextResponse, | ||
| submitWorkItems, | ||
| submitWorkItemResult, | ||
| submitSynthesizedResult, | ||
| submitPhaseOutput, | ||
| submitDecisions, | ||
| approvePlan, | ||
| adjustPlan, | ||
| resolveVerifyGate, | ||
| stopWorkflow, | ||
| statePath, | ||
| saveState, | ||
| loadState, | ||
| listStates | ||
| }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
72786
0.53%1820
0.39%8
14.29%2
100%