diff --git a/scripts/harness/orch-support.mjs b/scripts/harness/orch-support.mjs new file mode 100644 index 0000000..e6ee147 --- /dev/null +++ b/scripts/harness/orch-support.mjs @@ -0,0 +1,186 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { repoRoot } from "./core.mjs"; + +const orchExecutableName = process.platform === "win32" ? "orch.exe" : "orch"; + +export const defaultDbPath = path.join(repoRoot, ".artifacts", "orch", "coord.db"); +export const defaultWorkspaceRoot = path.join(repoRoot, ".artifacts", "orch", "worktrees"); +export const defaultTaskBodyRoot = path.join(repoRoot, ".artifacts", "orch", "task-bodies"); +export const legacyOrchBinPath = path.join( + os.homedir(), + ".codex", + "skills", + "orch", + "assets", + orchExecutableName +); + +function searchPathForExecutable(pathValue, existsSync = fs.existsSync) { + if (!pathValue) { + return null; + } + + for (const entry of pathValue.split(path.delimiter)) { + if (!entry) { + continue; + } + + const candidate = path.join(entry, orchExecutableName); + + if (existsSync(candidate)) { + return candidate; + } + } + + return null; +} + +export function resolveOrchBinary({ env = process.env, existsSync = fs.existsSync } = {}) { + const checkedPaths = []; + const explicitPath = env.CADENCE_UI_ORCH_BIN ? path.resolve(env.CADENCE_UI_ORCH_BIN) : null; + + if (explicitPath) { + checkedPaths.push(explicitPath); + + if (existsSync(explicitPath)) { + return { + binaryPath: explicitPath, + checkedPaths, + source: "CADENCE_UI_ORCH_BIN" + }; + } + } + + const pathMatch = searchPathForExecutable(env.PATH ?? "", existsSync); + + if (pathMatch) { + checkedPaths.push(pathMatch); + return { + binaryPath: pathMatch, + checkedPaths, + source: "PATH" + }; + } + + checkedPaths.push(legacyOrchBinPath); + + if (existsSync(legacyOrchBinPath)) { + return { + binaryPath: legacyOrchBinPath, + checkedPaths, + source: "legacy" + }; + } + + return { + binaryPath: null, + checkedPaths, + source: null + }; +} + +function parseTaskToken(token) { + const [taskIdPart, dependencyPart] = token.split("->").map((value) => value.trim()); + + return { + dependsOn: dependencyPart + ? dependencyPart + .split(",") + .map((value) => value.trim()) + .filter(Boolean) + : [], + id: taskIdPart + }; +} + +export function parsePlanTaskSketch(markdown) { + const lines = markdown.split(/\r?\n/); + const planTitle = + lines + .find((line) => line.startsWith("# ")) + ?.slice(2) + .trim() ?? "Execution Plan"; + const tasks = []; + let inTaskSketch = false; + + for (const line of lines) { + if (line.startsWith("## ")) { + if (inTaskSketch) { + break; + } + + inTaskSketch = line.trim() === "## Orchestration Task Sketch"; + continue; + } + + if (!inTaskSketch) { + continue; + } + + const match = line.match(/^\s*-\s+(?:`([^`]+)`|([^:]+)):\s+(.+)$/); + + if (!match) { + continue; + } + + const taskToken = (match[1] ?? match[2]).trim(); + const taskTitle = match[3].trim(); + const task = parseTaskToken(taskToken); + + tasks.push({ + ...task, + title: taskTitle + }); + } + + return { + planTitle, + tasks + }; +} + +export function readPlanFile(planFile) { + const absolutePath = path.isAbsolute(planFile) ? planFile : path.join(repoRoot, planFile); + const markdown = fs.readFileSync(absolutePath, "utf8"); + const parsed = parsePlanTaskSketch(markdown); + + return { + ...parsed, + absolutePath, + relativePath: path.relative(repoRoot, absolutePath) + }; +} + +export function renderTaskBody({ plan, task }) { + return `# ${task.id} — ${task.title} + +- Source plan: \`${plan.relativePath}\` +- Plan title: ${plan.planTitle} +- Task id: ${task.id} +- Dependencies: ${task.dependsOn.length > 0 ? task.dependsOn.join(", ") : "none"} + +## Execution Notes + +- Read the source execution plan before editing. +- Keep this task scoped to the work implied by the task title and dependencies. +- Run the narrowest useful harness suites first, then report what broader validation remains. +- Return any shared integration follow-up instead of editing unrelated surfaces opportunistically. +`; +} + +function sanitizeSegment(value) { + return value.replace(/[^a-zA-Z0-9._-]+/g, "-"); +} + +export function writeTaskBodyFile({ bodyRoot = defaultTaskBodyRoot, plan, runId, task }) { + const runDir = path.join(bodyRoot, sanitizeSegment(runId)); + const outputPath = path.join(runDir, `${sanitizeSegment(task.id)}.md`); + + fs.mkdirSync(runDir, { recursive: true }); + fs.writeFileSync(outputPath, `${renderTaskBody({ plan, task })}\n`, "utf8"); + + return outputPath; +} diff --git a/scripts/harness/orch-support.test.ts b/scripts/harness/orch-support.test.ts new file mode 100644 index 0000000..765f31c --- /dev/null +++ b/scripts/harness/orch-support.test.ts @@ -0,0 +1,134 @@ +// @vitest-environment node + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { + legacyOrchBinPath, + parsePlanTaskSketch, + resolveOrchBinary, + writeTaskBodyFile +} from "./orch-support.mjs"; + +const tempDirs: string[] = []; + +afterEach(() => { + for (const directory of tempDirs.splice(0)) { + fs.rmSync(directory, { force: true, recursive: true }); + } +}); + +function createTempDir(prefix: string) { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + tempDirs.push(directory); + return directory; +} + +describe("parsePlanTaskSketch", () => { + it("parses plan tasks and inline dependencies from the task sketch", () => { + const parsed = parsePlanTaskSketch(`# Example Plan + +## Orchestration Task Sketch + +- \`T1\`: stabilize smoke coverage +- \`T2 -> T1, T3\`: reconcile browser validation + +## Status Log +`); + + expect(parsed.planTitle).toBe("Example Plan"); + expect(parsed.tasks).toEqual([ + { + dependsOn: [], + id: "T1", + title: "stabilize smoke coverage" + }, + { + dependsOn: ["T1", "T3"], + id: "T2", + title: "reconcile browser validation" + } + ]); + }); +}); + +describe("resolveOrchBinary", () => { + it("prefers CADENCE_UI_ORCH_BIN when it points to an existing binary", () => { + const tempDir = createTempDir("cadence-orch-explicit-"); + const explicitBinary = path.join(tempDir, process.platform === "win32" ? "orch.exe" : "orch"); + + fs.writeFileSync(explicitBinary, "", "utf8"); + + expect( + resolveOrchBinary({ + env: { + CADENCE_UI_ORCH_BIN: explicitBinary, + PATH: "" + } + }) + ).toMatchObject({ + binaryPath: explicitBinary, + source: "CADENCE_UI_ORCH_BIN" + }); + }); + + it("falls back to PATH before the legacy location", () => { + const tempDir = createTempDir("cadence-orch-path-"); + const pathBinary = path.join(tempDir, process.platform === "win32" ? "orch.exe" : "orch"); + + fs.writeFileSync(pathBinary, "", "utf8"); + + expect( + resolveOrchBinary({ + env: { + PATH: tempDir + } + }) + ).toMatchObject({ + binaryPath: pathBinary, + source: "PATH" + }); + }); + + it("returns the checked locations when no binary exists", () => { + expect( + resolveOrchBinary({ + env: { + CADENCE_UI_ORCH_BIN: "/missing/orch", + PATH: "" + }, + existsSync: () => false + }) + ).toEqual({ + binaryPath: null, + checkedPaths: [path.resolve("/missing/orch"), legacyOrchBinPath], + source: null + }); + }); +}); + +describe("writeTaskBodyFile", () => { + it("writes a generated task body under the run-specific artifact directory", () => { + const bodyRoot = createTempDir("cadence-orch-bodies-"); + const outputPath = writeTaskBodyFile({ + bodyRoot, + plan: { + planTitle: "Harness Hardening", + relativePath: "docs/exec-plans/2026-03-24-harness-control-plane-hardening.md" + }, + runId: "cadence ui demo", + task: { + dependsOn: ["T0"], + id: "T1", + title: "stabilize changed-suite selection" + } + }); + + expect(outputPath).toContain(path.join("cadence-ui-demo", "T1.md")); + expect(fs.readFileSync(outputPath, "utf8")).toContain("# T1 — stabilize changed-suite selection"); + expect(fs.readFileSync(outputPath, "utf8")).toContain("Dependencies: T0"); + }); +}); diff --git a/scripts/harness/orchestrate.mjs b/scripts/harness/orchestrate.mjs index 928bee9..c09ced8 100644 --- a/scripts/harness/orchestrate.mjs +++ b/scripts/harness/orchestrate.mjs @@ -3,23 +3,28 @@ import path from "node:path"; import { execFileSync, spawnSync } from "node:child_process"; import { repoRoot } from "./core.mjs"; - -const orchBin = - process.env.CADENCE_UI_ORCH_BIN ?? "/Users/xd/.codex/skills/orch/assets/orch"; -const defaultDbPath = path.join(repoRoot, ".artifacts", "orch", "coord.db"); -const defaultWorkspaceRoot = path.join(repoRoot, ".artifacts", "orch", "worktrees"); +import { + defaultDbPath, + defaultWorkspaceRoot, + readPlanFile, + resolveOrchBinary, + writeTaskBodyFile +} from "./orch-support.mjs"; function printHelp() { process.stdout.write(`Cadence UI orchestration wrapper Usage: pnpm harness:orch -- [flags] + pnpm harness:orch -- doctor [--plan-file docs/exec-plans/foo.md] + pnpm harness:orch -- plan inspect --plan-file docs/exec-plans/foo.md + pnpm harness:orch -- plan body --run cadence_ui_demo --task T1 --plan-file docs/exec-plans/foo.md Examples: pnpm harness:orch -- run init --run cadence_ui_demo --goal "Refine release UX" --summary "Break work into isolated tasks" pnpm harness:orch -- task add --run cadence_ui_demo --task T1 --title "Stabilize smoke tests" --summary "Fix Storybook smoke drift" - pnpm harness:orch -- dispatch --run cadence_ui_demo --task T1 --to default-worker --body-file docs/exec-plans/task-t1.md - pnpm harness:orch -- status --run cadence_ui_demo + pnpm harness:orch -- dispatch --run cadence_ui_demo --task T1 --to default-worker --plan-file docs/exec-plans/task-plan.md + pnpm harness:orch -- doctor --plan-file docs/exec-plans/task-plan.md Defaults applied by this wrapper: --db ${path.relative(repoRoot, defaultDbPath)} @@ -27,13 +32,45 @@ Defaults applied by this wrapper: dispatch --workspace-root ${path.relative(repoRoot, defaultWorkspaceRoot)} dispatch --strict-worktree dispatch --base-ref + +Plan-linked helpers: + doctor Resolve the orch binary, show defaults, and inspect an execution plan. + plan inspect Print orchestration tasks parsed from a plan's task sketch. + plan body Generate a task body file from a plan and task id. + dispatch --plan-file Generate --body-file automatically from the referenced execution plan. `); } +function fail(message) { + process.stderr.write(`[harness:orch] ${message}\n`); + process.exit(1); +} + function hasFlag(args, flag) { return args.includes(flag); } +function getFlagValue(args, flag) { + const index = args.indexOf(flag); + + if (index === -1) { + return null; + } + + return args[index + 1] ?? null; +} + +function consumeFlagValue(args, flag) { + const index = args.indexOf(flag); + + if (index === -1) { + return null; + } + + const [value] = args.splice(index, 2).slice(1); + return value ?? null; +} + function getCurrentBranch() { try { return execFileSync("git", ["branch", "--show-current"], { @@ -45,6 +82,112 @@ function getCurrentBranch() { } } +function printPlanTasks(plan) { + process.stdout.write(`[harness:orch] Plan: ${plan.relativePath}\n`); + process.stdout.write(`[harness:orch] Title: ${plan.planTitle}\n`); + + if (plan.tasks.length === 0) { + process.stdout.write("[harness:orch] No orchestration task sketch entries found.\n"); + return; + } + + process.stdout.write("[harness:orch] Parsed tasks:\n"); + + for (const task of plan.tasks) { + const dependencySuffix = task.dependsOn.length > 0 ? ` (depends on ${task.dependsOn.join(", ")})` : ""; + process.stdout.write(`- ${task.id}: ${task.title}${dependencySuffix}\n`); + } +} + +function findPlanTask(plan, taskId) { + return plan.tasks.find((task) => task.id === taskId) ?? null; +} + +function maybeHandleDoctor(rawArgs) { + if (rawArgs[0] !== "doctor") { + return false; + } + + const planFile = getFlagValue(rawArgs, "--plan-file"); + const resolution = resolveOrchBinary(); + + process.stdout.write("[harness:orch] Doctor\n"); + process.stdout.write(`- repo root: ${repoRoot}\n`); + process.stdout.write(`- default db: ${path.relative(repoRoot, defaultDbPath)}\n`); + process.stdout.write(`- default workspace root: ${path.relative(repoRoot, defaultWorkspaceRoot)}\n`); + process.stdout.write(`- current branch: ${getCurrentBranch()}\n`); + process.stdout.write( + `- orch binary: ${resolution.binaryPath ? `${resolution.binaryPath} (${resolution.source})` : "not found"}\n` + ); + + if (!resolution.binaryPath) { + process.stdout.write("- checked locations:\n"); + + for (const checkedPath of resolution.checkedPaths) { + process.stdout.write(` - ${checkedPath}\n`); + } + + process.stdout.write( + "[harness:orch] Set CADENCE_UI_ORCH_BIN to an installed orch binary or add `orch` to PATH.\n" + ); + } + + if (planFile) { + const plan = readPlanFile(planFile); + printPlanTasks(plan); + } + + process.exit(resolution.binaryPath ? 0 : 1); +} + +function maybeHandlePlanCommand(rawArgs) { + if (rawArgs[0] !== "plan") { + return false; + } + + const subcommand = rawArgs[1]; + const planFile = getFlagValue(rawArgs, "--plan-file"); + + if (!planFile) { + fail("Expected --plan-file for plan commands."); + } + + const plan = readPlanFile(planFile); + + if (subcommand === "inspect") { + printPlanTasks(plan); + process.exit(0); + } + + if (subcommand === "body") { + const taskId = getFlagValue(rawArgs, "--task"); + const runId = getFlagValue(rawArgs, "--run") ?? "adhoc"; + + if (!taskId) { + fail("Expected --task when generating a plan-backed task body."); + } + + const task = findPlanTask(plan, taskId); + + if (!task) { + fail(`Task ${taskId} was not found in ${plan.relativePath}.`); + } + + const outputPath = writeTaskBodyFile({ + plan, + runId, + task + }); + + process.stdout.write( + `[harness:orch] Wrote ${path.relative(repoRoot, outputPath)} from ${plan.relativePath}\n` + ); + process.exit(0); + } + + fail(`Unknown plan subcommand: ${subcommand ?? ""}`); +} + const rawArgs = process.argv.slice(2).filter((value) => value !== "--"); if ( @@ -57,6 +200,9 @@ if ( process.exit(0); } +maybeHandleDoctor(rawArgs); +maybeHandlePlanCommand(rawArgs); + fs.mkdirSync(path.dirname(defaultDbPath), { recursive: true }); fs.mkdirSync(defaultWorkspaceRoot, { recursive: true }); @@ -69,6 +215,35 @@ if (!hasFlag(orchArgs, "--db")) { } if (command === "dispatch") { + const planFile = consumeFlagValue(orchArgs, "--plan-file"); + + if (planFile && !hasFlag(orchArgs, "--body-file")) { + const taskId = getFlagValue(orchArgs, "--task"); + const runId = getFlagValue(orchArgs, "--run") ?? "adhoc"; + + if (!taskId) { + fail("Expected --task when using dispatch --plan-file."); + } + + const plan = readPlanFile(planFile); + const task = findPlanTask(plan, taskId); + + if (!task) { + fail(`Task ${taskId} was not found in ${plan.relativePath}.`); + } + + const outputPath = writeTaskBodyFile({ + plan, + runId, + task + }); + + process.stdout.write( + `[harness:orch] Generated ${path.relative(repoRoot, outputPath)} from ${plan.relativePath}\n` + ); + orchArgs.push("--body-file", outputPath); + } + if (!hasFlag(orchArgs, "--repo-path")) { orchArgs.push("--repo-path", repoRoot); } @@ -86,7 +261,15 @@ if (command === "dispatch") { } } -const result = spawnSync(orchBin, orchArgs, { +const resolution = resolveOrchBinary(); + +if (!resolution.binaryPath) { + fail( + `Unable to locate orch. Checked ${resolution.checkedPaths.join(", ")}. Set CADENCE_UI_ORCH_BIN or add orch to PATH.` + ); +} + +const result = spawnSync(resolution.binaryPath, orchArgs, { cwd: repoRoot, stdio: "inherit" });