@smithers-orchestrator/cli 0.18.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.
- package/package.json +16 -16
- package/src/workflow-pack.js +506 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Smithers command-line interface, TUI, MCP server, and local workflow tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -34,21 +34,21 @@
|
|
|
34
34
|
"picocolors": "^1.1.1",
|
|
35
35
|
"react": "^19.2.5",
|
|
36
36
|
"zod": "^4.3.6",
|
|
37
|
-
"@smithers-orchestrator/
|
|
38
|
-
"@smithers-orchestrator/agents": "0.
|
|
39
|
-
"@smithers-orchestrator/
|
|
40
|
-
"@smithers-orchestrator/
|
|
41
|
-
"@smithers-orchestrator/
|
|
42
|
-
"@smithers-orchestrator/
|
|
43
|
-
"@smithers-orchestrator/
|
|
44
|
-
"@smithers-orchestrator/
|
|
45
|
-
"@smithers-orchestrator/memory": "0.
|
|
46
|
-
"@smithers-orchestrator/
|
|
47
|
-
"@smithers-orchestrator/openapi": "0.
|
|
48
|
-
"@smithers-orchestrator/
|
|
49
|
-
"@smithers-orchestrator/scheduler": "0.
|
|
50
|
-
"@smithers-orchestrator/
|
|
51
|
-
"@smithers-orchestrator/
|
|
37
|
+
"@smithers-orchestrator/components": "0.19.0",
|
|
38
|
+
"@smithers-orchestrator/agents": "0.19.0",
|
|
39
|
+
"@smithers-orchestrator/db": "0.19.0",
|
|
40
|
+
"@smithers-orchestrator/devtools": "0.19.0",
|
|
41
|
+
"@smithers-orchestrator/accounts": "0.19.0",
|
|
42
|
+
"@smithers-orchestrator/engine": "0.19.0",
|
|
43
|
+
"@smithers-orchestrator/driver": "0.19.0",
|
|
44
|
+
"@smithers-orchestrator/errors": "0.19.0",
|
|
45
|
+
"@smithers-orchestrator/memory": "0.19.0",
|
|
46
|
+
"@smithers-orchestrator/observability": "0.19.0",
|
|
47
|
+
"@smithers-orchestrator/openapi": "0.19.0",
|
|
48
|
+
"@smithers-orchestrator/protocol": "0.19.0",
|
|
49
|
+
"@smithers-orchestrator/scheduler": "0.19.0",
|
|
50
|
+
"@smithers-orchestrator/server": "0.19.0",
|
|
51
|
+
"@smithers-orchestrator/time-travel": "0.19.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/bun": "latest",
|
package/src/workflow-pack.js
CHANGED
|
@@ -88,6 +88,8 @@ function readOwnPackageVersion() {
|
|
|
88
88
|
*/
|
|
89
89
|
const BUNDLED_VERSION_PINS = {
|
|
90
90
|
zod: "4.3.6",
|
|
91
|
+
react: "19.2.5",
|
|
92
|
+
reactDom: "19.2.5",
|
|
91
93
|
typescript: "5.9.3",
|
|
92
94
|
reactTypes: "19.2.14",
|
|
93
95
|
reactDomTypes: "19.2.3",
|
|
@@ -101,6 +103,8 @@ function readDependencyVersions() {
|
|
|
101
103
|
return {
|
|
102
104
|
smithersVersion: readOwnPackageVersion(),
|
|
103
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),
|
|
104
108
|
typescriptVersion: resolveInstalledPackageVersion("typescript", BUNDLED_VERSION_PINS.typescript),
|
|
105
109
|
reactTypesVersion: resolveInstalledPackageVersion("@types/react", BUNDLED_VERSION_PINS.reactTypes),
|
|
106
110
|
reactDomTypesVersion: resolveInstalledPackageVersion("@types/react-dom", BUNDLED_VERSION_PINS.reactDomTypes),
|
|
@@ -121,11 +125,14 @@ function renderPackageJson(versions) {
|
|
|
121
125
|
type: "module",
|
|
122
126
|
scripts: {
|
|
123
127
|
typecheck: "tsc --noEmit",
|
|
128
|
+
gateway: "bun ./gateway.ts",
|
|
124
129
|
"workflow:list": "smithers workflow list",
|
|
125
130
|
"workflow:run": "smithers workflow run",
|
|
126
131
|
"workflow:implement": "smithers workflow implement",
|
|
127
132
|
},
|
|
128
133
|
dependencies: {
|
|
134
|
+
react: versions.reactVersion,
|
|
135
|
+
"react-dom": versions.reactDomVersion,
|
|
129
136
|
skills: "github:mattpocock/skills",
|
|
130
137
|
"smithers-orchestrator": smithersSpec,
|
|
131
138
|
zod: versions.zodVersion,
|
|
@@ -1626,6 +1633,462 @@ function renderWorkflowFile(id, displayName, body) {
|
|
|
1626
1633
|
].join("\n"),
|
|
1627
1634
|
};
|
|
1628
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
|
+
}
|
|
1629
2092
|
/**
|
|
1630
2093
|
* @returns {TemplateFile[]}
|
|
1631
2094
|
*/
|
|
@@ -1641,7 +2104,12 @@ function renderWorkflows() {
|
|
|
1641
2104
|
'import { ValidationLoop, implementOutputSchema, validateOutputSchema } from "../components/ValidationLoop";',
|
|
1642
2105
|
'import { reviewOutputSchema } from "../components/Review";',
|
|
1643
2106
|
"",
|
|
2107
|
+
"const inputSchema = z.object({",
|
|
2108
|
+
' prompt: z.string().default("Implement the requested change."),',
|
|
2109
|
+
"});",
|
|
2110
|
+
"",
|
|
1644
2111
|
"const { Workflow, smithers } = createSmithers({",
|
|
2112
|
+
" input: inputSchema,",
|
|
1645
2113
|
" implement: implementOutputSchema,",
|
|
1646
2114
|
" validate: validateOutputSchema,",
|
|
1647
2115
|
" review: reviewOutputSchema,",
|
|
@@ -2626,7 +3094,21 @@ function renderWorkflows() {
|
|
|
2626
3094
|
" summary: z.string(),",
|
|
2627
3095
|
"});",
|
|
2628
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
|
+
"",
|
|
2629
3109
|
"const { Workflow, Task, smithers, outputs } = createSmithers({",
|
|
3110
|
+
" input: inputSchema,",
|
|
3111
|
+
" tickets: ticketListSchema,",
|
|
2630
3112
|
" implement: implementOutputSchema,",
|
|
2631
3113
|
" validate: validateOutputSchema,",
|
|
2632
3114
|
" review: reviewOutputSchema,",
|
|
@@ -2650,6 +3132,16 @@ function renderWorkflows() {
|
|
|
2650
3132
|
" }",
|
|
2651
3133
|
"}",
|
|
2652
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
|
+
"",
|
|
2653
3145
|
"/** Build feedback string from validation + review outputs for a ticket. */",
|
|
2654
3146
|
"function buildFeedback(",
|
|
2655
3147
|
" ctx: any,",
|
|
@@ -2696,12 +3188,22 @@ function renderWorkflows() {
|
|
|
2696
3188
|
"",
|
|
2697
3189
|
"export default smithers((ctx) => {",
|
|
2698
3190
|
" const tickets = discoverTickets();",
|
|
2699
|
-
" const maxConcurrency =
|
|
3191
|
+
" const maxConcurrency = ctx.input.maxConcurrency;",
|
|
2700
3192
|
" const ticketResults = ctx.outputs.ticketResult ?? [];",
|
|
2701
3193
|
"",
|
|
2702
3194
|
" return (",
|
|
2703
3195
|
' <Workflow name="kanban">',
|
|
2704
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
|
+
"",
|
|
2705
3207
|
" {/* Implement each ticket in its own worktree branch, in parallel */}",
|
|
2706
3208
|
" <Parallel maxConcurrency={maxConcurrency}>",
|
|
2707
3209
|
" {tickets.map((ticket) => {",
|
|
@@ -2798,6 +3300,7 @@ function renderTemplateFiles(versions, env) {
|
|
|
2798
3300
|
path: ".smithers/preload.ts",
|
|
2799
3301
|
contents: ['import { mdxPlugin } from "smithers-orchestrator";', "", "mdxPlugin();", ""].join("\n"),
|
|
2800
3302
|
},
|
|
3303
|
+
renderGatewayFile(),
|
|
2801
3304
|
...renderAgentScaffoldFiles(),
|
|
2802
3305
|
{
|
|
2803
3306
|
path: ".smithers/agents.ts",
|
|
@@ -2819,6 +3322,7 @@ function renderTemplateFiles(versions, env) {
|
|
|
2819
3322
|
...renderPrompts(),
|
|
2820
3323
|
...renderComponents(),
|
|
2821
3324
|
...renderWorkflows(),
|
|
3325
|
+
renderKanbanUiFile(),
|
|
2822
3326
|
{
|
|
2823
3327
|
path: ".smithers/tickets/.gitkeep",
|
|
2824
3328
|
contents: "",
|
|
@@ -2847,6 +3351,7 @@ export function initWorkflowPack(options = {}) {
|
|
|
2847
3351
|
const env = process.env;
|
|
2848
3352
|
ensureDir(resolve(rootDir, "prompts"));
|
|
2849
3353
|
ensureDir(resolve(rootDir, "components"));
|
|
3354
|
+
ensureDir(resolve(rootDir, "ui"));
|
|
2850
3355
|
ensureDir(resolve(rootDir, "workflows"));
|
|
2851
3356
|
ensureDir(resolve(rootDir, "tickets"));
|
|
2852
3357
|
const executionsDir = resolve(rootDir, "executions");
|