@smithers-orchestrator/cli 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { createRequire } from "node:module";
2
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
4
  import { dirname, resolve } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
@@ -19,6 +20,9 @@ import { generateAgentsTs } from "./agent-detection.js";
19
20
  * @typedef {{ path: string; contents: string; preserveExisting?: boolean; }} TemplateFile
20
21
  */
21
22
 
23
+ const FALLBACK_SMITHERS_SPEC = "latest";
24
+ const require = createRequire(import.meta.url);
25
+
22
26
  /**
23
27
  * @param {string} path
24
28
  */
@@ -50,26 +54,33 @@ function readPackageVersion(path, fallback) {
50
54
  }
51
55
  }
52
56
  /**
53
- * Resolve `<spec>/package.json` via Node's module resolution (from this file's
54
- * location) and return its `version`. Uses `import.meta.resolve` so it works
55
- * whether the CLI is running from the monorepo checkout (deps under
56
- * `apps/cli/node_modules/`) or installed flat under a user's project
57
- * `node_modules/`. Falls back to the pin bundled at release time when the
58
- * package isn't installed in the user's project (typical for devDep-only
59
- * specs like `typescript` and `@types/*`).
57
+ * Resolve an installed dependency version from the current package layout.
60
58
  *
61
- * @param {string} spec
59
+ * @param {string} specifier
62
60
  * @param {string} fallback
63
61
  */
64
- function resolveDependencyVersion(spec, fallback) {
62
+ function resolveInstalledPackageVersion(specifier, fallback) {
65
63
  try {
66
- const url = import.meta.resolve(`${spec}/package.json`);
67
- return readPackageVersion(fileURLToPath(url), fallback);
64
+ const resolved = require.resolve(`${specifier}/package.json`);
65
+ return readPackageVersion(resolved, fallback);
68
66
  }
69
67
  catch {
70
68
  return fallback;
71
69
  }
72
70
  }
71
+ /**
72
+ * @returns {string | undefined}
73
+ */
74
+ function readOwnPackageVersion() {
75
+ try {
76
+ const ownPackagePath = fileURLToPath(new URL("../package.json", import.meta.url));
77
+ const version = readJson(ownPackagePath).version;
78
+ return typeof version === "string" && version.length > 0 ? version : undefined;
79
+ }
80
+ catch {
81
+ return undefined;
82
+ }
83
+ }
73
84
  /**
74
85
  * Pins shipped with this release for devDep-only specs that won't be in the
75
86
  * user's `node_modules` after `bunx smithers-orchestrator@latest init`. Bump
@@ -77,6 +88,8 @@ function resolveDependencyVersion(spec, fallback) {
77
88
  */
78
89
  const BUNDLED_VERSION_PINS = {
79
90
  zod: "4.3.6",
91
+ react: "19.2.5",
92
+ reactDom: "19.2.5",
80
93
  typescript: "5.9.3",
81
94
  reactTypes: "19.2.14",
82
95
  reactDomTypes: "19.2.3",
@@ -88,32 +101,40 @@ const BUNDLED_VERSION_PINS = {
88
101
  */
89
102
  function readDependencyVersions() {
90
103
  return {
91
- smithersVersion: readPackageVersion(fileURLToPath(new URL("../package.json", import.meta.url)), "0.0.0"),
92
- zodVersion: resolveDependencyVersion("zod", BUNDLED_VERSION_PINS.zod),
93
- typescriptVersion: resolveDependencyVersion("typescript", BUNDLED_VERSION_PINS.typescript),
94
- reactTypesVersion: resolveDependencyVersion("@types/react", BUNDLED_VERSION_PINS.reactTypes),
95
- reactDomTypesVersion: resolveDependencyVersion("@types/react-dom", BUNDLED_VERSION_PINS.reactDomTypes),
96
- mdxTypesVersion: resolveDependencyVersion("@types/mdx", BUNDLED_VERSION_PINS.mdxTypes),
97
- nodeTypesVersion: resolveDependencyVersion("@types/node", BUNDLED_VERSION_PINS.nodeTypes),
104
+ smithersVersion: readOwnPackageVersion(),
105
+ zodVersion: resolveInstalledPackageVersion("zod", BUNDLED_VERSION_PINS.zod),
106
+ reactVersion: resolveInstalledPackageVersion("react", BUNDLED_VERSION_PINS.react),
107
+ reactDomVersion: resolveInstalledPackageVersion("react-dom", BUNDLED_VERSION_PINS.reactDom),
108
+ typescriptVersion: resolveInstalledPackageVersion("typescript", BUNDLED_VERSION_PINS.typescript),
109
+ reactTypesVersion: resolveInstalledPackageVersion("@types/react", BUNDLED_VERSION_PINS.reactTypes),
110
+ reactDomTypesVersion: resolveInstalledPackageVersion("@types/react-dom", BUNDLED_VERSION_PINS.reactDomTypes),
111
+ mdxTypesVersion: resolveInstalledPackageVersion("@types/mdx", BUNDLED_VERSION_PINS.mdxTypes),
112
+ nodeTypesVersion: resolveInstalledPackageVersion("@types/node", BUNDLED_VERSION_PINS.nodeTypes),
98
113
  };
99
114
  }
100
115
  /**
101
116
  * @param {DependencyVersions} versions
102
117
  */
103
118
  function renderPackageJson(versions) {
119
+ const smithersSpec = versions.smithersVersion
120
+ ? `^${versions.smithersVersion}`
121
+ : FALLBACK_SMITHERS_SPEC;
104
122
  return JSON.stringify({
105
123
  name: "smithers-workflows",
106
124
  private: true,
107
125
  type: "module",
108
126
  scripts: {
109
127
  typecheck: "tsc --noEmit",
128
+ gateway: "bun ./gateway.ts",
110
129
  "workflow:list": "smithers workflow list",
111
130
  "workflow:run": "smithers workflow run",
112
131
  "workflow:implement": "smithers workflow implement",
113
132
  },
114
133
  dependencies: {
134
+ react: versions.reactVersion,
135
+ "react-dom": versions.reactDomVersion,
115
136
  skills: "github:mattpocock/skills",
116
- "smithers-orchestrator": "latest",
137
+ "smithers-orchestrator": smithersSpec,
117
138
  zod: versions.zodVersion,
118
139
  },
119
140
  devDependencies: {
@@ -590,6 +611,183 @@ function renderPrompts() {
590
611
  "",
591
612
  ].join("\n"),
592
613
  },
614
+ {
615
+ path: ".smithers/prompts/mission-plan.mdx",
616
+ contents: [
617
+ "# Mission Plan",
618
+ "",
619
+ "You are the mission orchestrator for a long-running Smithers workflow.",
620
+ "Scope the goal with the user before execution. If critical requirements, constraints, or acceptance criteria are missing, ask one question at a time using the ask-user command from the instructions you were given.",
621
+ "",
622
+ "Design the mission as serial milestones with targeted parallelism inside each milestone.",
623
+ "Each milestone must be a meaningful checkpoint that can be validated before the next milestone begins.",
624
+ "Each feature should be narrow enough for a fresh worker session to execute without needing the full mission history.",
625
+ "Include explicit validation checks for every milestone: tests, lint/typecheck/build commands, integration checks, and UI/browser walkthroughs when the repo has an app surface.",
626
+ "Capture risks, assumptions, out-of-scope items, and anything the user should approve before work starts.",
627
+ "",
628
+ "REQUEST:",
629
+ "{props.prompt}",
630
+ "",
631
+ "LIMITS:",
632
+ "- Max milestones: {props.maxMilestones}",
633
+ "- Max features per milestone: {props.maxFeaturesPerMilestone}",
634
+ "",
635
+ "REQUIRED OUTPUT:",
636
+ "{props.schema}",
637
+ "",
638
+ ].join("\n"),
639
+ },
640
+ {
641
+ path: ".smithers/prompts/mission-worker.mdx",
642
+ contents: [
643
+ "# Mission Worker",
644
+ "",
645
+ "You are a focused feature worker in a larger mission. Treat this as a fresh context window: use the mission plan below, execute only your assigned feature, and keep handoff notes precise.",
646
+ "",
647
+ "Rules:",
648
+ "1. Stay within the assigned feature scope unless you must make a small adjacent change to keep the repo working.",
649
+ "2. Prefer existing repo patterns and run the most relevant checks you can.",
650
+ "3. Record files changed, commands run, unresolved issues, and reusable learnings for later workers.",
651
+ "4. If the feature cannot be completed, make the best safe partial progress and explain exactly what blocks it.",
652
+ "",
653
+ "MISSION GOAL:",
654
+ "{props.missionGoal}",
655
+ "",
656
+ "MILESTONE:",
657
+ "```json",
658
+ "{JSON.stringify(props.milestone, null, 2)}",
659
+ "```",
660
+ "",
661
+ "FEATURE:",
662
+ "```json",
663
+ "{JSON.stringify(props.feature, null, 2)}",
664
+ "```",
665
+ "",
666
+ "{props.previousSummary ? `PREVIOUS MILESTONE SUMMARY:\\n${props.previousSummary}` : \"\"}",
667
+ "",
668
+ "REQUIRED OUTPUT:",
669
+ "{props.schema}",
670
+ "",
671
+ ].join("\n"),
672
+ },
673
+ {
674
+ path: ".smithers/prompts/mission-integrate.mdx",
675
+ contents: [
676
+ "# Mission Integrate",
677
+ "",
678
+ "You are integrating feature-worker results for one milestone.",
679
+ "",
680
+ "{props.useWorktrees ? \"The feature work may live on per-feature worktree branches. Inspect the branches, merge successful work back into the main workspace, resolve conflicts carefully, and leave conflicted or unsafe branches unmerged with a clear explanation.\" : \"Feature workers ran in the main workspace. Inspect their results and make any small integration fixes needed before validation.\"}",
681
+ "",
682
+ "MISSION GOAL:",
683
+ "{props.missionGoal}",
684
+ "",
685
+ "MILESTONE:",
686
+ "```json",
687
+ "{JSON.stringify(props.milestone, null, 2)}",
688
+ "```",
689
+ "",
690
+ "FEATURE RESULTS:",
691
+ "```json",
692
+ "{JSON.stringify(props.results, null, 2)}",
693
+ "```",
694
+ "",
695
+ "REQUIRED OUTPUT:",
696
+ "{props.schema}",
697
+ "",
698
+ ].join("\n"),
699
+ },
700
+ {
701
+ path: ".smithers/prompts/mission-validate.mdx",
702
+ contents: [
703
+ "# Mission Validate",
704
+ "",
705
+ "You are a validation worker for a mission milestone.",
706
+ "Validate accumulated work before the orchestrator moves to the next milestone.",
707
+ "",
708
+ "Run the strongest checks that fit the repo: tests, lint, typecheck, build, smoke tests, and integration checks.",
709
+ "If the repo has a UI, launch it and exercise core flows like a user would. Check render correctness, navigation, state transitions, and obvious layout regressions.",
710
+ "If a check cannot run because the repo lacks commands or setup, report that as a validation limitation instead of inventing results.",
711
+ "",
712
+ "MISSION GOAL:",
713
+ "{props.missionGoal}",
714
+ "",
715
+ "MILESTONE:",
716
+ "```json",
717
+ "{JSON.stringify(props.milestone, null, 2)}",
718
+ "```",
719
+ "",
720
+ "INTEGRATION RESULT:",
721
+ "```json",
722
+ "{JSON.stringify(props.integration, null, 2)}",
723
+ "```",
724
+ "",
725
+ "{props.followUp ? `FOLLOW-UP RESULT:\\n${JSON.stringify(props.followUp, null, 2)}` : \"\"}",
726
+ "",
727
+ "REQUIRED OUTPUT:",
728
+ "{props.schema}",
729
+ "",
730
+ ].join("\n"),
731
+ },
732
+ {
733
+ path: ".smithers/prompts/mission-follow-up.mdx",
734
+ contents: [
735
+ "# Mission Follow-Up",
736
+ "",
737
+ "Validation found issues in the current milestone. Fix the concrete regressions and gaps before the mission proceeds.",
738
+ "Keep changes targeted. Do not begin the next milestone.",
739
+ "",
740
+ "MISSION GOAL:",
741
+ "{props.missionGoal}",
742
+ "",
743
+ "MILESTONE:",
744
+ "```json",
745
+ "{JSON.stringify(props.milestone, null, 2)}",
746
+ "```",
747
+ "",
748
+ "VALIDATION RESULT:",
749
+ "```json",
750
+ "{JSON.stringify(props.validation, null, 2)}",
751
+ "```",
752
+ "",
753
+ "REQUIRED OUTPUT:",
754
+ "{props.schema}",
755
+ "",
756
+ ].join("\n"),
757
+ },
758
+ {
759
+ path: ".smithers/prompts/mission-final.mdx",
760
+ contents: [
761
+ "# Mission Final Report",
762
+ "",
763
+ "Write the final mission report. Summarize what shipped, what was validated, what remains risky, and the recommended next actions.",
764
+ "Be concrete about files, commands, validation gaps, and any milestone that did not pass validation.",
765
+ "",
766
+ "MISSION PLAN:",
767
+ "```json",
768
+ "{JSON.stringify(props.plan, null, 2)}",
769
+ "```",
770
+ "",
771
+ "FEATURE RESULTS:",
772
+ "```json",
773
+ "{JSON.stringify(props.featureResults, null, 2)}",
774
+ "```",
775
+ "",
776
+ "INTEGRATION RESULTS:",
777
+ "```json",
778
+ "{JSON.stringify(props.integrationResults, null, 2)}",
779
+ "```",
780
+ "",
781
+ "VALIDATION RESULTS:",
782
+ "```json",
783
+ "{JSON.stringify(props.validationResults, null, 2)}",
784
+ "```",
785
+ "",
786
+ "REQUIRED OUTPUT:",
787
+ "{props.schema}",
788
+ "",
789
+ ].join("\n"),
790
+ },
593
791
  {
594
792
  path: ".smithers/prompts/tickets-create.mdx",
595
793
  contents: [
@@ -1435,6 +1633,462 @@ function renderWorkflowFile(id, displayName, body) {
1435
1633
  ].join("\n"),
1436
1634
  };
1437
1635
  }
1636
+ function renderGatewayFile() {
1637
+ return {
1638
+ path: ".smithers/gateway.ts",
1639
+ contents: [
1640
+ 'import { Gateway, mdxPlugin } from "smithers-orchestrator";',
1641
+ 'import { dirname, resolve } from "node:path";',
1642
+ 'import { fileURLToPath } from "node:url";',
1643
+ "",
1644
+ "mdxPlugin();",
1645
+ "",
1646
+ "const here = dirname(fileURLToPath(import.meta.url));",
1647
+ "const projectRoot = resolve(here, \"..\");",
1648
+ "process.chdir(projectRoot);",
1649
+ "",
1650
+ "const { default: kanban } = await import(\"./workflows/kanban.tsx\");",
1651
+ "",
1652
+ "const parsedPort = Number(process.env.PORT ?? \"7331\");",
1653
+ "const port = Number.isInteger(parsedPort) && parsedPort > 0 ? parsedPort : 7331;",
1654
+ "const host = process.env.HOST ?? \"127.0.0.1\";",
1655
+ "",
1656
+ "const gateway = new Gateway({ heartbeatMs: 15_000 });",
1657
+ "",
1658
+ "function registerWorkflow(key: string, workflow: any, options?: any) {",
1659
+ " gateway.register(key, workflow, options);",
1660
+ "}",
1661
+ "",
1662
+ "registerWorkflow(\"kanban\", kanban, {",
1663
+ " ui: {",
1664
+ " entry: resolve(here, \"ui\", \"kanban.tsx\"),",
1665
+ " title: \"Kanban\",",
1666
+ " },",
1667
+ "});",
1668
+ "",
1669
+ "await gateway.listen({ host, port });",
1670
+ "console.log(\"Smithers Gateway listening on http://\" + host + \":\" + port);",
1671
+ "console.log(\"Kanban UI mounted at http://\" + host + \":\" + port + \"/workflows/kanban\");",
1672
+ "",
1673
+ ].join("\n"),
1674
+ };
1675
+ }
1676
+ function renderKanbanUiFile() {
1677
+ return {
1678
+ path: ".smithers/ui/kanban.tsx",
1679
+ contents: [
1680
+ "/** @jsxImportSource react */",
1681
+ 'import { useMemo, useState } from "react";',
1682
+ "import {",
1683
+ " createGatewayReactRoot,",
1684
+ " useGatewayActions,",
1685
+ " useGatewayApprovals,",
1686
+ " useGatewayNodeOutput,",
1687
+ " useGatewayRunEvents,",
1688
+ " useGatewayRuns,",
1689
+ '} from "smithers-orchestrator/gateway-react";',
1690
+ "",
1691
+ 'const WORKFLOW_KEY = "kanban";',
1692
+ "",
1693
+ "type RunSummary = {",
1694
+ " runId: string;",
1695
+ " workflowKey?: string;",
1696
+ " status?: string;",
1697
+ " createdAtMs?: number;",
1698
+ " startedAtMs?: number;",
1699
+ "};",
1700
+ "",
1701
+ 'type BoardLane = "pending" | "in-progress" | "completed";',
1702
+ 'type TicketState = "pending" | "in-progress" | "finished" | "failed";',
1703
+ "",
1704
+ "type TicketSummary = {",
1705
+ " id: string;",
1706
+ " slug: string;",
1707
+ " title: string;",
1708
+ "};",
1709
+ "",
1710
+ "type TicketView = {",
1711
+ " id: string;",
1712
+ " slug: string;",
1713
+ " title: string;",
1714
+ " lane: BoardLane;",
1715
+ " state: TicketState;",
1716
+ " events: number;",
1717
+ " currentStep?: string;",
1718
+ " nodeId?: string;",
1719
+ "};",
1720
+ "",
1721
+ "const laneLabels: Record<BoardLane, string> = {",
1722
+ ' pending: "Todo",',
1723
+ ' "in-progress": "In Progress",',
1724
+ ' completed: "Done",',
1725
+ "};",
1726
+ "",
1727
+ "const laneOrder: BoardLane[] = [\"pending\", \"in-progress\", \"completed\"];",
1728
+ "",
1729
+ "const styles = [",
1730
+ ' ":root { --bg: #0c0c0e; --panel: #151518; --card: #1c1c1f; --text: #eeeeee; --muted: #8a8a8e; --border: #262629; --primary: #5e6ad2; --success: #4ade80; --error: #f87171; --warning: #fbbf24; color-scheme: dark; font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, Helvetica, Arial, sans-serif; }",',
1731
+ ' "* { box-sizing: border-box; -webkit-font-smoothing: antialiased; }",',
1732
+ ' "body { margin: 0; background: var(--bg); color: var(--text); font-size: 13px; line-height: 1.4; }",',
1733
+ ' "button, input { font: inherit; transition: all 0.1s ease; }",',
1734
+ ' ".shell { height: 100vh; display: flex; flex-direction: column; overflow: hidden; }",',
1735
+ ' ".topbar { display: flex; align-items: center; justify-content: space-between; padding: 12px 20px; border-bottom: 1px solid var(--border); background: var(--bg); z-index: 10; }",',
1736
+ ' ".title-group { display: flex; align-items: center; gap: 12px; }",',
1737
+ ' "h1 { margin: 0; font-size: 14px; font-weight: 600; }",',
1738
+ ' ".run-indicator { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); background: var(--panel); padding: 4px 10px; border-radius: 6px; border: 1px solid var(--border); }",',
1739
+ ' ".toolbar { display: flex; align-items: center; gap: 12px; }",',
1740
+ ' ".field { display: flex; align-items: center; gap: 8px; }",',
1741
+ ' ".field label { color: var(--muted); font-size: 11px; font-weight: 600; text-transform: uppercase; }",',
1742
+ ' ".field input { width: 32px; border: 0; outline: none; color: var(--text); background: transparent; font-weight: 600; text-align: center; border-bottom: 1px solid var(--border); }",',
1743
+ ' ".button { display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--border); border-radius: 6px; height: 28px; padding: 0 12px; background: var(--panel); color: var(--text); cursor: pointer; font-weight: 500; font-size: 12px; }",',
1744
+ ' ".button:hover { background: var(--card); border-color: #3f3f46; }",',
1745
+ ' ".button.primary { background: var(--primary); color: white; border-color: var(--primary); }",',
1746
+ ' ".button.primary:hover { opacity: 0.9; }",',
1747
+ ' ".button.danger { color: var(--error); }",',
1748
+ ' ".button:disabled { opacity: 0.4; cursor: not-allowed; }",',
1749
+ ' ".main { display: grid; grid-template-columns: 1fr 280px; flex: 1; overflow: hidden; }",',
1750
+ ' ".board { display: grid; grid-template-columns: repeat(3, 1fr); background: var(--border); gap: 1px; overflow-x: auto; height: 100%; }",',
1751
+ ' ".lane { background: var(--bg); display: flex; flex-direction: column; min-width: 300px; height: 100%; }",',
1752
+ ' ".lane-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; position: sticky; top: 0; background: var(--bg); z-index: 5; }",',
1753
+ ' ".lane-title-wrap { display: flex; align-items: center; gap: 8px; }",',
1754
+ ' ".lane-title { font-weight: 600; font-size: 12px; }",',
1755
+ ' ".count { color: var(--muted); font-size: 12px; }",',
1756
+ ' ".status-circle { width: 14px; height: 14px; border: 1.5px solid var(--muted); border-radius: 50%; display: inline-block; position: relative; }",',
1757
+ ' ".lane.in-progress .status-circle { border-color: var(--warning); border-left-color: transparent; }",',
1758
+ ' ".lane.completed .status-circle { border-color: var(--primary); background: var(--primary); }",',
1759
+ ' ".lane.completed .status-circle::after { content: \'\'; position: absolute; left: 3px; top: 1px; width: 4px; height: 7px; border: solid white; border-width: 0 1.5px 1.5px 0; transform: rotate(45deg); }",',
1760
+ ' ".cards { padding: 8px; display: flex; flex-direction: column; gap: 4px; overflow-y: auto; flex: 1; }",',
1761
+ ' ".ticket { background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 12px; display: flex; flex-direction: column; gap: 10px; transition: border-color 0.1s; }",',
1762
+ ' ".ticket:hover { border-color: #3f3f46; }",',
1763
+ ' ".ticket-id { font-family: monospace; font-size: 11px; color: var(--muted); }",',
1764
+ ' ".ticket-title { font-size: 13px; font-weight: 500; color: var(--text); line-height: 1.4; }",',
1765
+ ' ".ticket-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }",',
1766
+ ' ".pill { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; background: #262629; color: var(--muted); border: 1px solid transparent; }",',
1767
+ ' ".pill.active { border-color: rgba(94, 106, 210, 0.4); color: #8e96ff; background: rgba(94, 106, 210, 0.1); }",',
1768
+ ' ".ticket-step { font-size: 11px; color: var(--muted); display: flex; align-items: center; gap: 6px; }",',
1769
+ ' ".dot { width: 6px; height: 6px; border-radius: 50%; background: var(--border); }",',
1770
+ ' ".ticket.in-progress .dot { background: var(--warning); box-shadow: 0 0 6px var(--warning); }",',
1771
+ ' ".ticket.failed .dot { background: var(--error); }",',
1772
+ ' ".ticket.finished .dot { background: var(--success); }",',
1773
+ ' ".sidebar { border-left: 1px solid var(--border); display: flex; flex-direction: column; background: var(--bg); overflow: hidden; }",',
1774
+ ' ".side-block { display: flex; flex-direction: column; flex: 1; overflow: hidden; border-bottom: 1px solid var(--border); }",',
1775
+ ' ".side-head { padding: 12px 16px; border-bottom: 1px solid var(--border); }",',
1776
+ ' ".side-head h2 { margin: 0; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }",',
1777
+ ' ".side-list { overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 2px; }",',
1778
+ ' ".run-btn { display: flex; flex-direction: column; gap: 4px; padding: 8px 10px; border-radius: 6px; cursor: pointer; border: 1px solid transparent; text-align: left; background: transparent; width: 100%; color: inherit; }",',
1779
+ ' ".run-btn:hover { background: var(--panel); }",',
1780
+ ' ".run-btn.selected { background: var(--panel); border-color: var(--border); }",',
1781
+ ' ".run-info { display: flex; align-items: center; justify-content: space-between; }",',
1782
+ ' ".run-name { font-family: monospace; font-weight: 600; font-size: 12px; }",',
1783
+ ' ".run-time { font-size: 11px; color: var(--muted); }",',
1784
+ ' ".approval-box { padding: 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--panel); margin-bottom: 8px; display: flex; flex-direction: column; gap: 8px; }",',
1785
+ ' ".approval-txt { font-weight: 600; font-size: 12px; }",',
1786
+ ' ".approval-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }",',
1787
+ ' ".empty { flex: 1; display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 12px; font-style: italic; opacity: 0.5; }",',
1788
+ ' ".toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); padding: 8px 16px; border-radius: 8px; background: var(--card); border: 1px solid var(--border); box-shadow: 0 8px 24px rgba(0,0,0,0.5); display: flex; align-items: center; gap: 12px; z-index: 100; font-size: 12px; font-weight: 500; }",',
1789
+ ' ".error-msg { color: var(--error); }",',
1790
+ '].join(\"\\n\");',
1791
+ "",
1792
+ "function isRecord(value: unknown): value is Record<string, unknown> {",
1793
+ " return typeof value === \"object\" && value !== null;",
1794
+ "}",
1795
+ "",
1796
+ "function asString(value: unknown): string | undefined {",
1797
+ " return typeof value === \"string\" ? value : undefined;",
1798
+ "}",
1799
+ "",
1800
+ "function asNumber(value: unknown): number | undefined {",
1801
+ " return typeof value === \"number\" && Number.isFinite(value) ? value : undefined;",
1802
+ "}",
1803
+ "",
1804
+ "function shortRunId(runId: string | undefined) {",
1805
+ " return runId ? runId.slice(0, 8) : \"none\";",
1806
+ "}",
1807
+ "",
1808
+ "function formatTime(ms: number | undefined) {",
1809
+ " if (!ms) return \"--\";",
1810
+ " const d = new Date(ms);",
1811
+ " return d.toLocaleDateString([], { month: \'short\', day: \'numeric\' }) + \" \" + d.toLocaleTimeString([], { hour: \'2-digit\', minute: \'2-digit\' });",
1812
+ "}",
1813
+ "",
1814
+ "function titleFromSlug(slug: string) {",
1815
+ " return slug",
1816
+ " .replace(/__/g, \" / \")",
1817
+ " .replace(/[-_]+/g, \" \")",
1818
+ " .replace(/\\b\\w/g, (letter) => letter.toUpperCase());",
1819
+ "}",
1820
+ "",
1821
+ "function extractDiscoveredTickets(value: unknown): TicketSummary[] {",
1822
+ " const response = isRecord(value) ? value : {};",
1823
+ " const row = isRecord(response.row) ? response.row : {};",
1824
+ " const rawTickets = Array.isArray(row.tickets) ? row.tickets : [];",
1825
+ " return rawTickets.flatMap((ticket): TicketSummary[] => {",
1826
+ " if (!isRecord(ticket)) return [];",
1827
+ " const slug = asString(ticket.slug);",
1828
+ " const id = asString(ticket.id) ?? (slug ? slug + \".md\" : undefined);",
1829
+ " if (!slug || !id) return [];",
1830
+ " return [{ id, slug, title: asString(ticket.title) ?? titleFromSlug(slug) }];",
1831
+ " });",
1832
+ "}",
1833
+ "",
1834
+ "function parseTicketNode(nodeId: string): { slug: string; step: string; result: boolean } | null {",
1835
+ " if (nodeId.startsWith(\"result-\")) {",
1836
+ " const slug = nodeId.slice(\"result-\".length);",
1837
+ " return slug ? { slug, step: \"result\", result: true } : null;",
1838
+ " }",
1839
+ " const [slug, step] = nodeId.split(\":\");",
1840
+ " if (!slug || slug === \"tickets\" || slug === \"merge\" || step === \"loop\") return null;",
1841
+ " return { slug, step: step ?? \"process\", result: false };",
1842
+ "}",
1843
+ "",
1844
+ "function stepLabel(step: string) {",
1845
+ " if (step === \"implement\") return \"Implementing\";",
1846
+ " if (step === \"validate\") return \"Validating\";",
1847
+ " if (step === \"review\") return \"Reviewing\";",
1848
+ " if (step === \"result\") return \"Done\";",
1849
+ " return titleFromSlug(step);",
1850
+ "}",
1851
+ "",
1852
+ "function collectStreamEvents(events: Array<Record<string, unknown>>) {",
1853
+ " return events",
1854
+ " .map((frame) => (isRecord(frame.payload) ? frame.payload : frame))",
1855
+ " .filter((frame): frame is Record<string, unknown> => isRecord(frame));",
1856
+ "}",
1857
+ "",
1858
+ "function deriveTickets(discovered: TicketSummary[], events: Array<Record<string, unknown>>): TicketView[] {",
1859
+ " const tickets = new Map<string, TicketView>();",
1860
+ " for (const ticket of discovered) {",
1861
+ " tickets.set(ticket.slug, {",
1862
+ " id: ticket.id,",
1863
+ " slug: ticket.slug,",
1864
+ " title: ticket.title,",
1865
+ " lane: \"pending\",",
1866
+ " state: \"pending\",",
1867
+ " events: 0,",
1868
+ " currentStep: \"Backlog\",",
1869
+ " });",
1870
+ " }",
1871
+ " for (const event of events) {",
1872
+ " const eventName = asString(event.event);",
1873
+ " if (eventName !== \"node.started\" && eventName !== \"node.finished\" && eventName !== \"node.failed\") continue;",
1874
+ " const payload = isRecord(event.payload) ? event.payload : {};",
1875
+ " const nodeId = asString(payload.nodeId);",
1876
+ " if (!nodeId) continue;",
1877
+ " const parsed = parseTicketNode(nodeId);",
1878
+ " if (!parsed) continue;",
1879
+ " const existing = tickets.get(parsed.slug) ?? {",
1880
+ " id: parsed.slug + \".md\",",
1881
+ " slug: parsed.slug,",
1882
+ " title: titleFromSlug(parsed.slug),",
1883
+ " lane: \"pending\" as BoardLane,",
1884
+ " state: \"pending\" as TicketState,",
1885
+ " events: 0,",
1886
+ " };",
1887
+ " if (eventName === \"node.failed\") {",
1888
+ " existing.lane = \"completed\";",
1889
+ " existing.state = \"failed\";",
1890
+ " existing.currentStep = \"Failed: \" + stepLabel(parsed.step);",
1891
+ " } else if (parsed.result && eventName === \"node.finished\") {",
1892
+ " existing.lane = \"completed\";",
1893
+ " existing.state = \"finished\";",
1894
+ " existing.currentStep = \"Completed\";",
1895
+ " } else {",
1896
+ " existing.lane = existing.lane === \"completed\" ? existing.lane : \"in-progress\";",
1897
+ " existing.state = existing.state === \"failed\" || existing.state === \"finished\" ? existing.state : \"in-progress\";",
1898
+ " existing.currentStep = stepLabel(parsed.step);",
1899
+ " }",
1900
+ " existing.nodeId = nodeId;",
1901
+ " existing.events += 1;",
1902
+ " tickets.set(parsed.slug, existing);",
1903
+ " }",
1904
+ " return Array.from(tickets.values()).sort((left, right) => left.title.localeCompare(right.title));",
1905
+ "}",
1906
+ "",
1907
+ "function App() {",
1908
+ " const [maxConcurrency, setMaxConcurrency] = useState(3);",
1909
+ " const [selectedRunId, setSelectedRunId] = useState<string>();",
1910
+ " const [busy, setBusy] = useState(false);",
1911
+ " const [message, setMessage] = useState(\"\");",
1912
+ " const [showMsg, setShowMsg] = useState(false);",
1913
+ " const runs = useGatewayRuns({ filter: { limit: 20 } });",
1914
+ " const approvals = useGatewayApprovals({ filter: { workflow: WORKFLOW_KEY, limit: 10 } });",
1915
+ " const actions = useGatewayActions();",
1916
+ "",
1917
+ " const kanbanRuns = useMemo(() => {",
1918
+ " return ((runs.data ?? []) as RunSummary[]).filter((run) => !run.workflowKey || run.workflowKey === WORKFLOW_KEY);",
1919
+ " }, [runs.data]);",
1920
+ " const activeRunId = selectedRunId ?? kanbanRuns[0]?.runId;",
1921
+ " const activeRun = kanbanRuns.find((run) => run.runId === activeRunId);",
1922
+ " const stream = useGatewayRunEvents(activeRunId, { afterSeq: 0 });",
1923
+ " const ticketsOutput = useGatewayNodeOutput({ runId: activeRunId, nodeId: \"tickets\", iteration: 0 });",
1924
+ " const streamEvents = useMemo(() => {",
1925
+ " return collectStreamEvents(stream.events as Array<Record<string, unknown>>)",
1926
+ " .filter((event) => !activeRunId || asString(event.runId) === activeRunId);",
1927
+ " }, [activeRunId, stream.events]);",
1928
+ " const discoveredTickets = useMemo(() => extractDiscoveredTickets(ticketsOutput.data), [ticketsOutput.data]);",
1929
+ " const tickets = useMemo(() => deriveTickets(discoveredTickets, streamEvents), [discoveredTickets, streamEvents]);",
1930
+ " const pendingApprovals = approvals.data ?? [];",
1931
+ " const ticketsByLane = useMemo(() => {",
1932
+ " const grouped: Record<BoardLane, TicketView[]> = { pending: [], \"in-progress\": [], completed: [] };",
1933
+ " for (const ticket of tickets) grouped[ticket.lane].push(ticket);",
1934
+ " return grouped;",
1935
+ " }, [tickets]);",
1936
+ "",
1937
+ " function notify(msg: string) {",
1938
+ " setMessage(msg);",
1939
+ " setShowMsg(true);",
1940
+ " setTimeout(() => setShowMsg(false), 3000);",
1941
+ " }",
1942
+ "",
1943
+ " async function refresh() {",
1944
+ " await Promise.all([runs.refetch(), approvals.refetch(), ticketsOutput.refetch()]);",
1945
+ " }",
1946
+ "",
1947
+ " async function launch() {",
1948
+ " setBusy(true);",
1949
+ " try {",
1950
+ " const run = await actions.launchRun({ workflow: WORKFLOW_KEY, input: { maxConcurrency } });",
1951
+ " setSelectedRunId(run.runId);",
1952
+ " notify(\"Launched \" + shortRunId(run.runId));",
1953
+ " await refresh();",
1954
+ " } catch (e) { notify(String(e)); } finally { setBusy(false); }",
1955
+ " }",
1956
+ "",
1957
+ " async function cancelRun() {",
1958
+ " if (!activeRunId) return;",
1959
+ " setBusy(true);",
1960
+ " try {",
1961
+ " await actions.cancelRun({ runId: activeRunId });",
1962
+ " notify(\"Cancelled run\");",
1963
+ " await refresh();",
1964
+ " } catch (e) { notify(String(e)); } finally { setBusy(false); }",
1965
+ " }",
1966
+ "",
1967
+ " async function decide(approval: (typeof pendingApprovals)[number], ok: boolean) {",
1968
+ " setBusy(true);",
1969
+ " try {",
1970
+ " await actions.submitApproval({",
1971
+ " runId: approval.runId, nodeId: approval.nodeId, iteration: approval.iteration,",
1972
+ " decision: { approved: ok, note: (ok ? \'Approved\' : \'Denied\') + \' via UI\' }",
1973
+ " });",
1974
+ " notify(ok ? \'Approved\' : \'Denied\');",
1975
+ " await refresh();",
1976
+ " } catch (e) { notify(String(e)); } finally { setBusy(false); }",
1977
+ " }",
1978
+ "",
1979
+ " const hasError = !!(stream.error || ticketsOutput.error);",
1980
+ "",
1981
+ " return (",
1982
+ " <main className=\"shell\">",
1983
+ " <style>{styles}</style>",
1984
+ " <header className=\"topbar\">",
1985
+ " <div className=\"title-group\">",
1986
+ " <h1>Kanban</h1>",
1987
+ " <div className=\"run-indicator\">",
1988
+ " <div className=\"status-circle\" style={{ border: \'1.5px solid var(--primary)\', background: activeRun?.status === \'running\' ? \'var(--primary)\' : \'transparent\' }}></div>",
1989
+ " <span>{activeRunId ? shortRunId(activeRunId) : \'No Run\'}</span>",
1990
+ " <span style={{ color: activeRun?.status === \'running\' ? \'var(--warning)\' : \'var(--muted)\', fontWeight: 600 }}>",
1991
+ " {activeRun?.status ?? \'Idle\'}",
1992
+ " </span>",
1993
+ " </div>",
1994
+ " </div>",
1995
+ " <div className=\"toolbar\">",
1996
+ " <div className=\"field\">",
1997
+ " <label>Limit</label>",
1998
+ " <input",
1999
+ " type=\"number\" min={1} max={10} value={maxConcurrency}",
2000
+ " onChange={(e) => setMaxConcurrency(Math.max(1, Number(e.currentTarget.value) || 1))}",
2001
+ " />",
2002
+ " </div>",
2003
+ " <button className=\"button\" onClick={() => void refresh()} disabled={busy}>Refresh</button>",
2004
+ " {activeRun?.status === \"running\" && (",
2005
+ " <button className=\"button danger\" onClick={() => void cancelRun()} disabled={busy}>Cancel</button>",
2006
+ " )}",
2007
+ " <button className=\"button primary\" onClick={() => void launch()} disabled={busy}>Launch Run</button>",
2008
+ " </div>",
2009
+ " </header>",
2010
+ "",
2011
+ " <div className=\"main\">",
2012
+ " <div className=\"board\">",
2013
+ " {laneOrder.map((lane) => (",
2014
+ " <section className={\"lane \" + lane} key={lane}>",
2015
+ " <div className=\"lane-head\">",
2016
+ " <div className=\"lane-title-wrap\">",
2017
+ " <div className=\"status-circle\"></div>",
2018
+ " <div className=\"lane-title\">{laneLabels[lane]}</div>",
2019
+ " <div className=\"count\">{ticketsByLane[lane].length}</div>",
2020
+ " </div>",
2021
+ " </div>",
2022
+ " <div className=\"cards\">",
2023
+ " {ticketsByLane[lane].map((t) => (",
2024
+ " <article className={\"ticket \" + t.state} key={t.slug}>",
2025
+ " <span className=\"ticket-id\">{t.slug.slice(0, 12).toUpperCase()}</span>",
2026
+ " <div className=\"ticket-title\">{t.title}</div>",
2027
+ " <div className=\"ticket-meta\">",
2028
+ " <div className=\"ticket-step\">",
2029
+ " <div className=\"dot\"></div>",
2030
+ " <span>{t.currentStep ?? t.state}</span>",
2031
+ " </div>",
2032
+ " <div className={\"pill \" + (t.state === \'in-progress\' ? \'active\' : \'\')}>{t.events} Events</div>",
2033
+ " </div>",
2034
+ " </article>",
2035
+ " ))}",
2036
+ " {ticketsByLane[lane].length === 0 && <div className=\"empty\">Empty</div>}",
2037
+ " </div>",
2038
+ " </section>",
2039
+ " ))}",
2040
+ " </div>",
2041
+ "",
2042
+ " <aside className=\"sidebar\">",
2043
+ " <section className=\"side-block\">",
2044
+ " <div className=\"side-head\"><h2>Recent Runs</h2></div>",
2045
+ " <div className=\"side-list\">",
2046
+ " {kanbanRuns.map((r) => (",
2047
+ " <button",
2048
+ " key={r.runId} className={\"run-btn \" + (r.runId === activeRunId ? \'selected\' : \'\')}",
2049
+ " onClick={() => setSelectedRunId(r.runId)}",
2050
+ " >",
2051
+ " <div className=\"run-info\">",
2052
+ " <span className=\"run-name\">{shortRunId(r.runId)}</span>",
2053
+ " <div className=\"pill\">{r.status}</div>",
2054
+ " </div>",
2055
+ " <span className=\"run-time\">{formatTime(asNumber(r.startedAtMs) ?? asNumber(r.createdAtMs))}</span>",
2056
+ " </button>",
2057
+ " ))}",
2058
+ " </div>",
2059
+ " </section>",
2060
+ " <section className=\"side-block\" style={{ flex: 1.5 }}>",
2061
+ " <div className=\"side-head\"><h2>Approvals</h2></div>",
2062
+ " <div className=\"side-list\">",
2063
+ " {pendingApprovals.map((a) => (",
2064
+ " <div className=\"approval-box\" key={a.runId + a.nodeId + a.iteration}>",
2065
+ " <div className=\"approval-txt\">{a.requestTitle ?? a.nodeId}</div>",
2066
+ " <div className=\"approval-grid\">",
2067
+ " <button className=\"button\" onClick={() => void decide(a, false)} disabled={busy}>Deny</button>",
2068
+ " <button className=\"button primary\" onClick={() => void decide(a, true)} disabled={busy}>Approve</button>",
2069
+ " </div>",
2070
+ " </div>",
2071
+ " ))}",
2072
+ " {pendingApprovals.length === 0 && <div className=\"empty\">All clear</div>}",
2073
+ " </div>",
2074
+ " </section>",
2075
+ " </aside>",
2076
+ " </div>",
2077
+ "",
2078
+ " {showMsg && (",
2079
+ " <div className=\"toast\">",
2080
+ " <div className={hasError ? \"error-msg\" : \"\"}>{stream.error?.message ?? ticketsOutput.error?.message ?? message}</div>",
2081
+ " </div>",
2082
+ " )}",
2083
+ " </main>",
2084
+ " );",
2085
+ "}",
2086
+ "",
2087
+ "createGatewayReactRoot(<App />);",
2088
+ "",
2089
+ ].join("\n"),
2090
+ };
2091
+ }
1438
2092
  /**
1439
2093
  * @returns {TemplateFile[]}
1440
2094
  */
@@ -1450,7 +2104,12 @@ function renderWorkflows() {
1450
2104
  'import { ValidationLoop, implementOutputSchema, validateOutputSchema } from "../components/ValidationLoop";',
1451
2105
  'import { reviewOutputSchema } from "../components/Review";',
1452
2106
  "",
2107
+ "const inputSchema = z.object({",
2108
+ ' prompt: z.string().default("Implement the requested change."),',
2109
+ "});",
2110
+ "",
1453
2111
  "const { Workflow, smithers } = createSmithers({",
2112
+ " input: inputSchema,",
1454
2113
  " implement: implementOutputSchema,",
1455
2114
  " validate: validateOutputSchema,",
1456
2115
  " review: reviewOutputSchema,",
@@ -1974,6 +2633,440 @@ function renderWorkflows() {
1974
2633
  " </Workflow>",
1975
2634
  "));",
1976
2635
  ]),
2636
+ renderWorkflowFile("mission", "Mission", [
2637
+ ...sharedImports,
2638
+ 'import AskUserInstructions from "../prompts/ask-user-instructions.mdx";',
2639
+ 'import MissionPlanPrompt from "../prompts/mission-plan.mdx";',
2640
+ 'import MissionWorkerPrompt from "../prompts/mission-worker.mdx";',
2641
+ 'import MissionIntegratePrompt from "../prompts/mission-integrate.mdx";',
2642
+ 'import MissionValidatePrompt from "../prompts/mission-validate.mdx";',
2643
+ 'import MissionFollowUpPrompt from "../prompts/mission-follow-up.mdx";',
2644
+ 'import MissionFinalPrompt from "../prompts/mission-final.mdx";',
2645
+ "",
2646
+ "const missionFeatureSchema = z.looseObject({",
2647
+ ' id: z.string().default("feature"),',
2648
+ ' title: z.string().default("Feature"),',
2649
+ ' instructions: z.string().default("Complete the assigned feature."),',
2650
+ " files: z.array(z.string()).default([]),",
2651
+ " validation: z.array(z.string()).default([]),",
2652
+ ' workerType: z.enum(["implementation", "test", "docs", "research"]).default("implementation"),',
2653
+ " canRunInParallel: z.boolean().default(true),",
2654
+ "});",
2655
+ "",
2656
+ "const missionMilestoneSchema = z.looseObject({",
2657
+ ' id: z.string().default("milestone"),',
2658
+ ' title: z.string().default("Milestone"),',
2659
+ ' objective: z.string().default("Complete this milestone."),',
2660
+ " features: z.array(missionFeatureSchema).default([]),",
2661
+ " validationPlan: z.array(z.string()).default([]),",
2662
+ "});",
2663
+ "",
2664
+ "const missionPlanSchema = z.looseObject({",
2665
+ ' goal: z.string().default(""),',
2666
+ ' summary: z.string().default("Mission plan created."),',
2667
+ " milestones: z.array(missionMilestoneSchema).default([]),",
2668
+ " assumptions: z.array(z.string()).default([]),",
2669
+ " risks: z.array(z.string()).default([]),",
2670
+ " outOfScope: z.array(z.string()).default([]),",
2671
+ ' approvalNotes: z.string().nullable().default(null),',
2672
+ "});",
2673
+ "",
2674
+ "const missionApprovalSchema = z.looseObject({",
2675
+ " approved: z.boolean().default(false),",
2676
+ " note: z.string().nullable().default(null),",
2677
+ " decidedBy: z.string().nullable().default(null),",
2678
+ " decidedAt: z.string().nullable().default(null),",
2679
+ "});",
2680
+ "",
2681
+ "const missionFeatureResultSchema = z.looseObject({",
2682
+ ' featureId: z.string().default("feature"),',
2683
+ ' status: z.enum(["success", "partial", "failed"]).default("partial"),',
2684
+ ' summary: z.string().default("Feature worker completed."),',
2685
+ " filesChanged: z.array(z.string()).default([]),",
2686
+ " commandsRun: z.array(z.string()).default([]),",
2687
+ " blockers: z.array(z.string()).default([]),",
2688
+ " reusableLearnings: z.array(z.string()).default([]),",
2689
+ "});",
2690
+ "",
2691
+ "const milestoneIntegrationSchema = z.looseObject({",
2692
+ ' milestoneId: z.string().default("milestone"),',
2693
+ ' status: z.enum(["integrated", "partial", "blocked"]).default("integrated"),',
2694
+ ' summary: z.string().default("Milestone integrated."),',
2695
+ " mergedBranches: z.array(z.string()).default([]),",
2696
+ " conflictedBranches: z.array(z.string()).default([]),",
2697
+ " filesChanged: z.array(z.string()).default([]),",
2698
+ "});",
2699
+ "",
2700
+ "const milestoneValidationSchema = z.looseObject({",
2701
+ ' milestoneId: z.string().default("milestone"),',
2702
+ " passed: z.boolean().default(true),",
2703
+ ' summary: z.string().default("Validation completed."),',
2704
+ " checks: z.array(z.object({",
2705
+ " name: z.string(),",
2706
+ ' status: z.enum(["passed", "failed", "skipped"]),',
2707
+ " details: z.string().nullable().default(null),",
2708
+ " })).default([]),",
2709
+ " regressions: z.array(z.string()).default([]),",
2710
+ " followUps: z.array(z.string()).default([]),",
2711
+ "});",
2712
+ "",
2713
+ "const missionFinalSchema = z.looseObject({",
2714
+ ' status: z.enum(["completed", "partial", "cancelled"]).default("completed"),',
2715
+ ' summary: z.string().default("Mission complete."),',
2716
+ " completedMilestones: z.number().int().default(0),",
2717
+ " totalMilestones: z.number().int().default(0),",
2718
+ " validationPassed: z.boolean().default(true),",
2719
+ " remainingRisks: z.array(z.string()).default([]),",
2720
+ " nextActions: z.array(z.string()).default([]),",
2721
+ " markdownBody: z.string().default(\"\"),",
2722
+ "});",
2723
+ "",
2724
+ "const inputSchema = z.object({",
2725
+ ' prompt: z.string().default("Describe the mission goal."),',
2726
+ " requirePlanApproval: z.boolean().default(true),",
2727
+ " maxMilestones: z.number().int().min(1).max(20).default(6),",
2728
+ " maxFeaturesPerMilestone: z.number().int().min(1).max(20).default(6),",
2729
+ " maxConcurrency: z.number().int().min(1).max(10).default(3),",
2730
+ " useWorktrees: z.boolean().default(false),",
2731
+ ' baseBranch: z.string().default("main"),',
2732
+ "});",
2733
+ "",
2734
+ "const { Workflow, Task, Sequence, Parallel, Approval, Worktree, smithers, outputs } = createSmithers({",
2735
+ " input: inputSchema,",
2736
+ " missionPlan: missionPlanSchema,",
2737
+ " missionApproval: missionApprovalSchema,",
2738
+ " missionFeature: missionFeatureResultSchema,",
2739
+ " milestoneIntegration: milestoneIntegrationSchema,",
2740
+ " milestoneValidation: milestoneValidationSchema,",
2741
+ " missionFinal: missionFinalSchema,",
2742
+ "});",
2743
+ "",
2744
+ 'const missionMemory = { kind: "workflow", id: "mission" } as const;',
2745
+ "",
2746
+ "function slugify(value: unknown, fallback: string): string {",
2747
+ ' const normalized = String(value ?? "")',
2748
+ " .toLowerCase()",
2749
+ ' .replace(/[^a-z0-9]+/g, "-")',
2750
+ ' .replace(/^-+|-+$/g, "");',
2751
+ " return normalized.length > 0 ? normalized : fallback;",
2752
+ "}",
2753
+ "",
2754
+ "function asStringArray(value: unknown): string[] {",
2755
+ " return Array.isArray(value) ? value.map((entry) => String(entry)).filter(Boolean) : [];",
2756
+ "}",
2757
+ "",
2758
+ "function normalizeFeature(feature: any, index: number): any {",
2759
+ " const title = typeof feature?.title === \"string\" && feature.title.length > 0",
2760
+ " ? feature.title",
2761
+ " : `Feature ${index + 1}`;",
2762
+ " return {",
2763
+ " id: slugify(feature?.id ?? title, `feature-${index + 1}`),",
2764
+ " title,",
2765
+ " instructions: typeof feature?.instructions === \"string\" && feature.instructions.length > 0",
2766
+ " ? feature.instructions",
2767
+ " : `Complete ${title}.`,",
2768
+ " files: asStringArray(feature?.files),",
2769
+ " validation: asStringArray(feature?.validation),",
2770
+ " workerType: [\"implementation\", \"test\", \"docs\", \"research\"].includes(feature?.workerType)",
2771
+ " ? feature.workerType",
2772
+ ' : "implementation",',
2773
+ " canRunInParallel: feature?.canRunInParallel !== false,",
2774
+ " };",
2775
+ "}",
2776
+ "",
2777
+ "function normalizeMilestones(plan: any, maxMilestones: number, maxFeaturesPerMilestone: number): any[] {",
2778
+ " return (Array.isArray(plan?.milestones) ? plan.milestones : [])",
2779
+ " .slice(0, maxMilestones)",
2780
+ " .map((milestone: any, index: number) => {",
2781
+ " const title = typeof milestone?.title === \"string\" && milestone.title.length > 0",
2782
+ " ? milestone.title",
2783
+ " : `Milestone ${index + 1}`;",
2784
+ " const features = (Array.isArray(milestone?.features) ? milestone.features : [])",
2785
+ " .slice(0, maxFeaturesPerMilestone)",
2786
+ " .map((feature: any, featureIndex: number) => normalizeFeature(feature, featureIndex));",
2787
+ " return {",
2788
+ " id: slugify(milestone?.id ?? title, `milestone-${index + 1}`),",
2789
+ " title,",
2790
+ " objective: typeof milestone?.objective === \"string\" && milestone.objective.length > 0",
2791
+ " ? milestone.objective",
2792
+ " : title,",
2793
+ " validationPlan: asStringArray(milestone?.validationPlan),",
2794
+ " features: features.length > 0",
2795
+ " ? features",
2796
+ " : [normalizeFeature({ title, instructions: milestone?.objective ?? title }, 0)],",
2797
+ " };",
2798
+ " });",
2799
+ "}",
2800
+ "",
2801
+ "function featureTaskId(milestoneIndex: number, feature: any): string {",
2802
+ " return `mission:milestone:${milestoneIndex + 1}:feature:${feature.id}`;",
2803
+ "}",
2804
+ "",
2805
+ "function milestoneIntegrateId(milestoneIndex: number): string {",
2806
+ " return `mission:milestone:${milestoneIndex + 1}:integrate`;",
2807
+ "}",
2808
+ "",
2809
+ "function milestoneValidationId(milestoneIndex: number): string {",
2810
+ " return `mission:milestone:${milestoneIndex + 1}:validate`;",
2811
+ "}",
2812
+ "",
2813
+ "function milestoneFollowUpId(milestoneIndex: number): string {",
2814
+ " return `mission:milestone:${milestoneIndex + 1}:follow-up`;",
2815
+ "}",
2816
+ "",
2817
+ "function milestoneRevalidationId(milestoneIndex: number): string {",
2818
+ " return `mission:milestone:${milestoneIndex + 1}:revalidate`;",
2819
+ "}",
2820
+ "",
2821
+ "function featureNeeds(milestoneIndex: number, features: any[]): Record<string, string> {",
2822
+ " return Object.fromEntries(features.map((feature, index) => [`feature${index}`, featureTaskId(milestoneIndex, feature)]));",
2823
+ "}",
2824
+ "",
2825
+ "function featureDeps(features: any[]): Record<string, typeof missionFeatureResultSchema> {",
2826
+ " return Object.fromEntries(features.map((_, index) => [`feature${index}`, outputs.missionFeature]));",
2827
+ "}",
2828
+ "",
2829
+ "function workerAgentsFor(feature: any): any {",
2830
+ " if (feature.workerType === \"research\") return agents.smartTool;",
2831
+ " if (feature.workerType === \"docs\") return agents.cheapFast;",
2832
+ " return agents.smart;",
2833
+ "}",
2834
+ "",
2835
+ "function previousMilestoneSummary(ctx: any): string {",
2836
+ " const integrations = ctx.outputs.milestoneIntegration ?? [];",
2837
+ " const validations = ctx.outputs.milestoneValidation ?? [];",
2838
+ " return [",
2839
+ " ...integrations.map((entry: any) => `Integration: ${entry.summary}`),",
2840
+ " ...validations.map((entry: any) => `Validation: ${entry.passed ? \"passed\" : \"failed\"} - ${entry.summary}`),",
2841
+ " ].slice(-8).join(\"\\n\");",
2842
+ "}",
2843
+ "",
2844
+ "function milestoneIsTerminal(ctx: any, milestoneIndex: number): boolean {",
2845
+ " const revalidation = ctx.outputMaybe(\"milestoneValidation\", { nodeId: milestoneRevalidationId(milestoneIndex) });",
2846
+ " if (revalidation) return true;",
2847
+ " const validation = ctx.outputMaybe(\"milestoneValidation\", { nodeId: milestoneValidationId(milestoneIndex) });",
2848
+ " return Boolean(validation && validation.passed !== false);",
2849
+ "}",
2850
+ "",
2851
+ "function activeMilestoneIndex(ctx: any, milestones: any[]): number {",
2852
+ " for (let index = 0; index < milestones.length; index += 1) {",
2853
+ " if (!milestoneIsTerminal(ctx, index)) return index;",
2854
+ " }",
2855
+ " return milestones.length;",
2856
+ "}",
2857
+ "",
2858
+ "function renderFeatureWorker(ctx: any, plan: any, milestone: any, milestoneIndex: number, feature: any) {",
2859
+ " const taskId = featureTaskId(milestoneIndex, feature);",
2860
+ " const workerTask = (",
2861
+ " <Task",
2862
+ " key={taskId}",
2863
+ " id={taskId}",
2864
+ " output={outputs.missionFeature}",
2865
+ " agent={workerAgentsFor(feature)}",
2866
+ " timeoutMs={3_600_000}",
2867
+ " heartbeatTimeoutMs={900_000}",
2868
+ " continueOnFail",
2869
+ " memory={{",
2870
+ " recall: { namespace: missionMemory, query: `${plan.goal} ${milestone.title} ${feature.title}`, topK: 5 },",
2871
+ " remember: { namespace: missionMemory, key: taskId },",
2872
+ " }}",
2873
+ " >",
2874
+ " <MissionWorkerPrompt",
2875
+ " missionGoal={plan.goal || ctx.input.prompt}",
2876
+ " milestone={milestone}",
2877
+ " feature={feature}",
2878
+ " previousSummary={previousMilestoneSummary(ctx)}",
2879
+ " />",
2880
+ " </Task>",
2881
+ " );",
2882
+ "",
2883
+ " if (!(ctx.input.useWorktrees ?? false)) return workerTask;",
2884
+ "",
2885
+ " return (",
2886
+ " <Worktree",
2887
+ " key={taskId}",
2888
+ " id={`mission-worktree-${milestoneIndex + 1}-${feature.id}`}",
2889
+ " path={`.worktrees/mission-${milestoneIndex + 1}-${feature.id}`}",
2890
+ " branch={`mission/${milestoneIndex + 1}/${feature.id}`}",
2891
+ " baseBranch={ctx.input.baseBranch ?? \"main\"}",
2892
+ " >",
2893
+ " {workerTask}",
2894
+ " </Worktree>",
2895
+ " );",
2896
+ "}",
2897
+ "",
2898
+ "function renderMilestone(ctx: any, plan: any, milestone: any, milestoneIndex: number) {",
2899
+ " const features = milestone.features;",
2900
+ " const integrationId = milestoneIntegrateId(milestoneIndex);",
2901
+ " const validationId = milestoneValidationId(milestoneIndex);",
2902
+ " const integration = ctx.outputMaybe(\"milestoneIntegration\", { nodeId: integrationId });",
2903
+ " const validation = ctx.outputMaybe(\"milestoneValidation\", { nodeId: validationId });",
2904
+ " const needsFollowUp = Boolean(validation && validation.passed === false);",
2905
+ "",
2906
+ " return (",
2907
+ " <Sequence>",
2908
+ " <Parallel maxConcurrency={Math.min(ctx.input.maxConcurrency ?? 3, features.length)}>",
2909
+ " {features.map((feature: any) => renderFeatureWorker(ctx, plan, milestone, milestoneIndex, feature))}",
2910
+ " </Parallel>",
2911
+ " <Task",
2912
+ " id={integrationId}",
2913
+ " output={outputs.milestoneIntegration}",
2914
+ " agent={agents.smartTool}",
2915
+ " needs={featureNeeds(milestoneIndex, features)}",
2916
+ " deps={featureDeps(features)}",
2917
+ " timeoutMs={1_800_000}",
2918
+ " memory={{ remember: { namespace: missionMemory, key: integrationId } }}",
2919
+ " >",
2920
+ " {(deps: any) => {",
2921
+ " const results = features.map((_: any, index: number) => deps[`feature${index}`]);",
2922
+ " return (",
2923
+ " <MissionIntegratePrompt",
2924
+ " missionGoal={plan.goal || ctx.input.prompt}",
2925
+ " milestone={milestone}",
2926
+ " results={results}",
2927
+ " useWorktrees={ctx.input.useWorktrees ?? false}",
2928
+ " />",
2929
+ " );",
2930
+ " }}",
2931
+ " </Task>",
2932
+ " <Task",
2933
+ " id={validationId}",
2934
+ " output={outputs.milestoneValidation}",
2935
+ " agent={agents.smart}",
2936
+ " needs={{ integration: integrationId }}",
2937
+ " deps={{ integration: outputs.milestoneIntegration }}",
2938
+ " timeoutMs={1_800_000}",
2939
+ " heartbeatTimeoutMs={900_000}",
2940
+ " memory={{ remember: { namespace: missionMemory, key: validationId } }}",
2941
+ " >",
2942
+ " {(deps: any) => (",
2943
+ " <MissionValidatePrompt",
2944
+ " missionGoal={plan.goal || ctx.input.prompt}",
2945
+ " milestone={milestone}",
2946
+ " integration={deps.integration}",
2947
+ " />",
2948
+ " )}",
2949
+ " </Task>",
2950
+ " {needsFollowUp && (",
2951
+ " <Sequence>",
2952
+ " <Task",
2953
+ " id={milestoneFollowUpId(milestoneIndex)}",
2954
+ " output={outputs.missionFeature}",
2955
+ " agent={agents.smart}",
2956
+ " needs={{ validation: validationId }}",
2957
+ " deps={{ validation: outputs.milestoneValidation }}",
2958
+ " timeoutMs={1_800_000}",
2959
+ " memory={{ remember: { namespace: missionMemory, key: milestoneFollowUpId(milestoneIndex) } }}",
2960
+ " >",
2961
+ " {(deps: any) => (",
2962
+ " <MissionFollowUpPrompt",
2963
+ " missionGoal={plan.goal || ctx.input.prompt}",
2964
+ " milestone={milestone}",
2965
+ " validation={deps.validation}",
2966
+ " />",
2967
+ " )}",
2968
+ " </Task>",
2969
+ " <Task",
2970
+ " id={milestoneRevalidationId(milestoneIndex)}",
2971
+ " output={outputs.milestoneValidation}",
2972
+ " agent={agents.smart}",
2973
+ " needs={{ followUp: milestoneFollowUpId(milestoneIndex) }}",
2974
+ " deps={{ followUp: outputs.missionFeature }}",
2975
+ " timeoutMs={1_800_000}",
2976
+ " heartbeatTimeoutMs={900_000}",
2977
+ " memory={{ remember: { namespace: missionMemory, key: milestoneRevalidationId(milestoneIndex) } }}",
2978
+ " >",
2979
+ " {(deps: any) => (",
2980
+ " <MissionValidatePrompt",
2981
+ " missionGoal={plan.goal || ctx.input.prompt}",
2982
+ " milestone={milestone}",
2983
+ " integration={integration}",
2984
+ " followUp={deps.followUp}",
2985
+ " />",
2986
+ " )}",
2987
+ " </Task>",
2988
+ " </Sequence>",
2989
+ " )}",
2990
+ " </Sequence>",
2991
+ " );",
2992
+ "}",
2993
+ "",
2994
+ "function renderFinal(ctx: any, plan: any, milestones: any[]) {",
2995
+ " return (",
2996
+ " <Task id=\"mission:final\" output={outputs.missionFinal} agent={agents.smartTool}>",
2997
+ " <MissionFinalPrompt",
2998
+ " plan={{ ...plan, milestones }}",
2999
+ " featureResults={ctx.outputs.missionFeature ?? []}",
3000
+ " integrationResults={ctx.outputs.milestoneIntegration ?? []}",
3001
+ " validationResults={ctx.outputs.milestoneValidation ?? []}",
3002
+ " />",
3003
+ " </Task>",
3004
+ " );",
3005
+ "}",
3006
+ "",
3007
+ "export default smithers((ctx) => {",
3008
+ " const plan = ctx.outputMaybe(\"missionPlan\", { nodeId: \"mission:plan\" });",
3009
+ " const approval = ctx.outputMaybe(\"missionApproval\", { nodeId: \"mission:approve-plan\" });",
3010
+ " const approvalRequired = ctx.input.requirePlanApproval;",
3011
+ " const approvalDenied = approvalRequired && approval && approval.approved === false;",
3012
+ " const approved = !approvalRequired || approval?.approved === true;",
3013
+ " const milestones = normalizeMilestones(plan, ctx.input.maxMilestones ?? 6, ctx.input.maxFeaturesPerMilestone ?? 6);",
3014
+ " const activeIndex = approved ? activeMilestoneIndex(ctx, milestones) : 0;",
3015
+ "",
3016
+ " return (",
3017
+ " <Workflow name=\"mission\">",
3018
+ " <Sequence>",
3019
+ " <Task",
3020
+ " id=\"mission:plan\"",
3021
+ " output={outputs.missionPlan}",
3022
+ " agent={agents.smartTool}",
3023
+ " timeoutMs={1_800_000}",
3024
+ " heartbeatTimeoutMs={900_000}",
3025
+ " memory={{ remember: { namespace: missionMemory, key: \"mission:plan\" } }}",
3026
+ " >",
3027
+ " <AskUserInstructions />",
3028
+ " <MissionPlanPrompt",
3029
+ " prompt={ctx.input.prompt}",
3030
+ " maxMilestones={ctx.input.maxMilestones ?? 6}",
3031
+ " maxFeaturesPerMilestone={ctx.input.maxFeaturesPerMilestone ?? 6}",
3032
+ " />",
3033
+ " </Task>",
3034
+ "",
3035
+ " {plan && approvalRequired && !approval && (",
3036
+ " <Approval",
3037
+ " id=\"mission:approve-plan\"",
3038
+ " output={outputs.missionApproval}",
3039
+ " request={{",
3040
+ " title: \"Approve mission plan?\",",
3041
+ " summary: plan.summary || \"Review the scoped mission plan before workers begin.\",",
3042
+ " metadata: { milestones: milestones.length, risks: plan.risks ?? [] },",
3043
+ " }}",
3044
+ " onDeny=\"continue\"",
3045
+ " />",
3046
+ " )}",
3047
+ "",
3048
+ " {approvalDenied && (",
3049
+ " <Task id=\"mission:cancelled\" output={outputs.missionFinal}>",
3050
+ " {{",
3051
+ " status: \"cancelled\",",
3052
+ " summary: `Mission plan was not approved. ${approval?.note ?? \"\"}`.trim(),",
3053
+ " completedMilestones: 0,",
3054
+ " totalMilestones: milestones.length,",
3055
+ " validationPassed: false,",
3056
+ " remainingRisks: plan?.risks ?? [],",
3057
+ " nextActions: [\"Revise the mission scope and rerun the workflow.\"],",
3058
+ " markdownBody: \"# Mission Cancelled\\n\\nThe plan was not approved.\",",
3059
+ " }}",
3060
+ " </Task>",
3061
+ " )}",
3062
+ "",
3063
+ " {plan && approved && activeIndex < milestones.length && renderMilestone(ctx, plan, milestones[activeIndex], activeIndex)}",
3064
+ " {plan && approved && activeIndex >= milestones.length && renderFinal(ctx, plan, milestones)}",
3065
+ " </Sequence>",
3066
+ " </Workflow>",
3067
+ " );",
3068
+ "});",
3069
+ ]),
1977
3070
  {
1978
3071
  path: ".smithers/workflows/kanban.tsx",
1979
3072
  contents: [
@@ -2001,7 +3094,21 @@ function renderWorkflows() {
2001
3094
  " summary: z.string(),",
2002
3095
  "});",
2003
3096
  "",
3097
+ "const inputSchema = z.object({",
3098
+ " maxConcurrency: z.number().int().min(1).max(10).default(3),",
3099
+ "});",
3100
+ "",
3101
+ "const ticketListSchema = z.object({",
3102
+ " tickets: z.array(z.object({",
3103
+ " id: z.string(),",
3104
+ " slug: z.string(),",
3105
+ " title: z.string(),",
3106
+ " })),",
3107
+ "});",
3108
+ "",
2004
3109
  "const { Workflow, Task, smithers, outputs } = createSmithers({",
3110
+ " input: inputSchema,",
3111
+ " tickets: ticketListSchema,",
2005
3112
  " implement: implementOutputSchema,",
2006
3113
  " validate: validateOutputSchema,",
2007
3114
  " review: reviewOutputSchema,",
@@ -2025,6 +3132,16 @@ function renderWorkflows() {
2025
3132
  " }",
2026
3133
  "}",
2027
3134
  "",
3135
+ "function ticketTitle(ticket: { id: string; slug: string; content: string }): string {",
3136
+ ' const heading = ticket.content.match(/^#\\s+(.+)$/m)?.[1]?.trim();',
3137
+ " return heading && heading.length > 0",
3138
+ " ? heading",
3139
+ " : ticket.slug",
3140
+ ' .replace(/__/g, " / ")',
3141
+ ' .replace(/[-_]+/g, " ")',
3142
+ " .replace(/\\b\\w/g, (letter) => letter.toUpperCase());",
3143
+ "}",
3144
+ "",
2028
3145
  "/** Build feedback string from validation + review outputs for a ticket. */",
2029
3146
  "function buildFeedback(",
2030
3147
  " ctx: any,",
@@ -2071,12 +3188,22 @@ function renderWorkflows() {
2071
3188
  "",
2072
3189
  "export default smithers((ctx) => {",
2073
3190
  " const tickets = discoverTickets();",
2074
- " const maxConcurrency = Number(ctx.input.maxConcurrency) || 3;",
3191
+ " const maxConcurrency = ctx.input.maxConcurrency;",
2075
3192
  " const ticketResults = ctx.outputs.ticketResult ?? [];",
2076
3193
  "",
2077
3194
  " return (",
2078
3195
  ' <Workflow name="kanban">',
2079
3196
  " <Sequence>",
3197
+ " <Task id=\"tickets\" output={outputs.tickets}>",
3198
+ " {{",
3199
+ " tickets: tickets.map((ticket) => ({",
3200
+ " id: ticket.id,",
3201
+ " slug: ticket.slug,",
3202
+ " title: ticketTitle(ticket),",
3203
+ " })),",
3204
+ " }}",
3205
+ " </Task>",
3206
+ "",
2080
3207
  " {/* Implement each ticket in its own worktree branch, in parallel */}",
2081
3208
  " <Parallel maxConcurrency={maxConcurrency}>",
2082
3209
  " {tickets.map((ticket) => {",
@@ -2173,6 +3300,7 @@ function renderTemplateFiles(versions, env) {
2173
3300
  path: ".smithers/preload.ts",
2174
3301
  contents: ['import { mdxPlugin } from "smithers-orchestrator";', "", "mdxPlugin();", ""].join("\n"),
2175
3302
  },
3303
+ renderGatewayFile(),
2176
3304
  ...renderAgentScaffoldFiles(),
2177
3305
  {
2178
3306
  path: ".smithers/agents.ts",
@@ -2194,6 +3322,7 @@ function renderTemplateFiles(versions, env) {
2194
3322
  ...renderPrompts(),
2195
3323
  ...renderComponents(),
2196
3324
  ...renderWorkflows(),
3325
+ renderKanbanUiFile(),
2197
3326
  {
2198
3327
  path: ".smithers/tickets/.gitkeep",
2199
3328
  contents: "",
@@ -2222,6 +3351,7 @@ export function initWorkflowPack(options = {}) {
2222
3351
  const env = process.env;
2223
3352
  ensureDir(resolve(rootDir, "prompts"));
2224
3353
  ensureDir(resolve(rootDir, "components"));
3354
+ ensureDir(resolve(rootDir, "ui"));
2225
3355
  ensureDir(resolve(rootDir, "workflows"));
2226
3356
  ensureDir(resolve(rootDir, "tickets"));
2227
3357
  const executionsDir = resolve(rootDir, "executions");
@@ -2291,8 +3421,10 @@ const WORKFLOW_FOLLOW_UPS = {
2291
3421
  "research": [
2292
3422
  { command: "workflow run write-a-prd", description: "Formalize findings into a PRD" },
2293
3423
  { command: "workflow run plan", description: "Turn research into an implementation plan" },
3424
+ { command: "workflow run mission", description: "Run a scoped long-horizon mission" },
2294
3425
  ],
2295
3426
  "plan": [
3427
+ { command: "workflow run mission", description: "Execute as a milestone-gated mission" },
2296
3428
  { command: "workflow run research-plan-implement", description: "Research, plan, and execute" },
2297
3429
  { command: "workflow run implement", description: "Execute the plan" },
2298
3430
  { command: "workflow run tickets-create", description: "Break plan into tickets" },
@@ -2315,6 +3447,7 @@ const WORKFLOW_FOLLOW_UPS = {
2315
3447
  { command: "workflow run tickets-create", description: "Break directly into tickets" },
2316
3448
  ],
2317
3449
  "write-a-prd": [
3450
+ { command: "workflow run mission", description: "Execute the PRD as a mission" },
2318
3451
  { command: "workflow run tickets-create", description: "Break PRD into implementable tickets" },
2319
3452
  { command: "workflow run plan", description: "Turn PRD into a phased plan" },
2320
3453
  { command: "workflow run implement", description: "Start building from the PRD" },
@@ -2331,6 +3464,11 @@ const WORKFLOW_FOLLOW_UPS = {
2331
3464
  { command: "workflow run review", description: "Review the changes" },
2332
3465
  { command: "workflow run improve-test-coverage", description: "Improve test coverage" },
2333
3466
  ],
3467
+ "mission": [
3468
+ { command: "workflow run review", description: "Review mission changes" },
3469
+ { command: "workflow run improve-test-coverage", description: "Fill validation gaps" },
3470
+ { command: "workflow run audit", description: "Audit completed feature areas" },
3471
+ ],
2334
3472
  "review": [
2335
3473
  { command: "workflow run implement", description: "Address review feedback" },
2336
3474
  ],