@smithers-orchestrator/components 0.16.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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +47 -0
  3. package/src/SmithersWorkflow.ts +1 -0
  4. package/src/aspects/AspectAccumulator.ts +9 -0
  5. package/src/aspects/AspectContext.js +29 -0
  6. package/src/aspects/AspectContextValue.ts +16 -0
  7. package/src/aspects/CostBudgetConfig.ts +9 -0
  8. package/src/aspects/LatencySloConfig.ts +11 -0
  9. package/src/aspects/TokenBudgetConfig.ts +11 -0
  10. package/src/aspects/TrackingConfig.ts +11 -0
  11. package/src/aspects/index.js +10 -0
  12. package/src/components/Approval.js +211 -0
  13. package/src/components/ApprovalAutoApprove.ts +8 -0
  14. package/src/components/ApprovalDecision.ts +4 -0
  15. package/src/components/ApprovalGate.js +45 -0
  16. package/src/components/ApprovalGateProps.ts +22 -0
  17. package/src/components/ApprovalMode.ts +1 -0
  18. package/src/components/ApprovalOption.ts +6 -0
  19. package/src/components/ApprovalProps.ts +42 -0
  20. package/src/components/ApprovalRanking.ts +4 -0
  21. package/src/components/ApprovalRequest.ts +5 -0
  22. package/src/components/ApprovalSelection.ts +4 -0
  23. package/src/components/Aspects.js +39 -0
  24. package/src/components/AspectsProps.ts +18 -0
  25. package/src/components/Branch.js +12 -0
  26. package/src/components/BranchProps.ts +8 -0
  27. package/src/components/CategoryConfig.ts +10 -0
  28. package/src/components/CheckConfig.ts +8 -0
  29. package/src/components/CheckSuite.js +71 -0
  30. package/src/components/CheckSuiteProps.ts +12 -0
  31. package/src/components/ClassifyAndRoute.js +75 -0
  32. package/src/components/ClassifyAndRouteProps.ts +30 -0
  33. package/src/components/ColumnDef.ts +19 -0
  34. package/src/components/ContentPipeline.js +38 -0
  35. package/src/components/ContentPipelineProps.ts +12 -0
  36. package/src/components/ContentPipelineStage.ts +13 -0
  37. package/src/components/ContinueAsNew.js +27 -0
  38. package/src/components/ContinueAsNewProps.ts +6 -0
  39. package/src/components/Debate.js +63 -0
  40. package/src/components/DebateProps.ts +15 -0
  41. package/src/components/DecisionRule.ts +10 -0
  42. package/src/components/DecisionTable.js +42 -0
  43. package/src/components/DecisionTableProps.ts +14 -0
  44. package/src/components/DepsSpec.ts +3 -0
  45. package/src/components/DriftDetector.js +54 -0
  46. package/src/components/DriftDetectorProps.ts +29 -0
  47. package/src/components/EscalationChain.js +99 -0
  48. package/src/components/EscalationChainProps.ts +20 -0
  49. package/src/components/EscalationLevel.ts +13 -0
  50. package/src/components/GatherAndSynthesize.js +69 -0
  51. package/src/components/GatherAndSynthesizeProps.ts +24 -0
  52. package/src/components/HumanTask.js +94 -0
  53. package/src/components/HumanTaskProps.ts +27 -0
  54. package/src/components/InferDeps.ts +8 -0
  55. package/src/components/Kanban.js +68 -0
  56. package/src/components/KanbanProps.ts +27 -0
  57. package/src/components/Loop.js +6 -0
  58. package/src/components/LoopProps.ts +11 -0
  59. package/src/components/MergeQueue.js +16 -0
  60. package/src/components/MergeQueueProps.ts +12 -0
  61. package/src/components/Optimizer.js +52 -0
  62. package/src/components/OptimizerProps.ts +25 -0
  63. package/src/components/OutputTarget.ts +6 -0
  64. package/src/components/Panel.js +69 -0
  65. package/src/components/PanelProps.ts +17 -0
  66. package/src/components/PanelistConfig.ts +7 -0
  67. package/src/components/Parallel.js +16 -0
  68. package/src/components/ParallelProps.ts +8 -0
  69. package/src/components/Poller.js +69 -0
  70. package/src/components/PollerProps.ts +24 -0
  71. package/src/components/Ralph.js +17 -0
  72. package/src/components/RalphProps.ts +4 -0
  73. package/src/components/ReviewLoop.js +51 -0
  74. package/src/components/ReviewLoopProps.ts +23 -0
  75. package/src/components/Runbook.js +91 -0
  76. package/src/components/RunbookProps.ts +19 -0
  77. package/src/components/RunbookStep.ts +17 -0
  78. package/src/components/Saga.js +77 -0
  79. package/src/components/SagaProps.ts +10 -0
  80. package/src/components/SagaStepDef.ts +8 -0
  81. package/src/components/SagaStepProps.ts +7 -0
  82. package/src/components/Sandbox.js +48 -0
  83. package/src/components/SandboxProps.ts +46 -0
  84. package/src/components/SandboxRuntime.ts +1 -0
  85. package/src/components/SandboxVolumeMount.ts +5 -0
  86. package/src/components/SandboxWorkspaceSpec.ts +6 -0
  87. package/src/components/ScanFixVerify.js +60 -0
  88. package/src/components/ScanFixVerifyProps.ts +30 -0
  89. package/src/components/Sequence.js +11 -0
  90. package/src/components/SequenceProps.ts +6 -0
  91. package/src/components/Signal.js +48 -0
  92. package/src/components/SignalProps.ts +21 -0
  93. package/src/components/SourceDef.ts +12 -0
  94. package/src/components/Subflow.js +32 -0
  95. package/src/components/SubflowProps.ts +33 -0
  96. package/src/components/SuperSmithers.js +102 -0
  97. package/src/components/SuperSmithersProps.ts +20 -0
  98. package/src/components/Supervisor.js +86 -0
  99. package/src/components/SupervisorProps.ts +28 -0
  100. package/src/components/Task.js +319 -0
  101. package/src/components/TaskProps.ts +57 -0
  102. package/src/components/Timer.js +42 -0
  103. package/src/components/TimerProps.ts +21 -0
  104. package/src/components/TryCatchFinally.js +35 -0
  105. package/src/components/TryCatchFinallyProps.ts +12 -0
  106. package/src/components/WaitForEvent.js +37 -0
  107. package/src/components/WaitForEventProps.ts +28 -0
  108. package/src/components/Workflow.js +10 -0
  109. package/src/components/WorkflowProps.ts +7 -0
  110. package/src/components/Worktree.js +17 -0
  111. package/src/components/WorktreeProps.ts +11 -0
  112. package/src/components/control-flow-utils.js +37 -0
  113. package/src/components/index.js +121 -0
  114. package/src/index.d.ts +1579 -0
  115. package/src/index.js +62 -0
  116. package/src/markdownComponents.js +44 -0
  117. package/src/renderMdx.js +26 -0
  118. package/src/types/react-dom-server.d.ts +1 -0
  119. package/src/types.ts +22 -0
  120. package/src/zod-to-example.js +87 -0
@@ -0,0 +1,319 @@
1
+ // @smithers-type-exports-begin
2
+ /**
3
+ * @template D
4
+ * @typedef {import("./InferDeps.ts").InferDeps<D>} InferDeps
5
+ */
6
+ /** @typedef {import("./OutputTarget.ts").OutputTarget} OutputTarget */
7
+ // @smithers-type-exports-end
8
+
9
+ import React from "react";
10
+ import { renderToStaticMarkup } from "react-dom/server";
11
+ import { markdownComponents } from "../markdownComponents.js";
12
+ import { zodSchemaToJsonExample } from "../zod-to-example.js";
13
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
14
+ import { SmithersContext } from "@smithers-orchestrator/react-reconciler/context";
15
+ import { AspectContext } from "../aspects/AspectContext.js";
16
+ import { ClaudeCodeAgent } from "@smithers-orchestrator/agents/ClaudeCodeAgent";
17
+ import { GeminiAgent } from "@smithers-orchestrator/agents/GeminiAgent";
18
+ import { PiAgent } from "@smithers-orchestrator/agents/PiAgent";
19
+ /** @typedef {import("@smithers-orchestrator/agents/AgentLike").AgentLike} AgentLike */
20
+ /** @typedef {import("./DepsSpec.ts").DepsSpec} DepsSpec */
21
+ /**
22
+ * @template Row, Output, D
23
+ * @typedef {import("./TaskProps.ts").TaskProps<Row, Output, D>} TaskProps
24
+ */
25
+
26
+ /**
27
+ * Render a prompt React node to plain markdown text.
28
+ *
29
+ * If the prompt is a React element (e.g. a compiled MDX component), we inject
30
+ * `markdownComponents` via the standard MDX `components` prop so that
31
+ * renderToStaticMarkup outputs clean markdown instead of HTML.
32
+ * No HTML tag stripping or entity decoding needed.
33
+ * @param {unknown} prompt
34
+ * @returns {string}
35
+ */
36
+ export function renderPromptToText(prompt) {
37
+ if (prompt == null)
38
+ return "";
39
+ if (typeof prompt === "string")
40
+ return prompt;
41
+ if (typeof prompt === "number")
42
+ return String(prompt);
43
+ try {
44
+ let element;
45
+ if (React.isValidElement(prompt)) {
46
+ // Inject markdown components into the element so MDX components
47
+ // render fragments instead of HTML tags.
48
+ element = React.cloneElement(prompt, {
49
+ components: markdownComponents,
50
+ });
51
+ }
52
+ else {
53
+ element = React.createElement(React.Fragment, null, prompt);
54
+ }
55
+ return renderToStaticMarkup(element)
56
+ .replace(/\n{3,}/g, "\n\n")
57
+ .trim();
58
+ }
59
+ catch (err) {
60
+ const result = String(prompt ?? "");
61
+ if (result === "[object Object]") {
62
+ throw new SmithersError("MDX_PRELOAD_INACTIVE", `MDX prompt could not be rendered — the prompt resolved to [object Object] instead of a React component.\n\n` +
63
+ `This usually means the MDX preload is not active. Common causes:\n` +
64
+ ` • bunfig.toml uses [run] preload instead of top-level preload (the [run] section doesn't apply to dynamic imports)\n` +
65
+ ` • bunfig.toml is not in the current working directory\n` +
66
+ ` • mdxPlugin() is not registered in the preload script\n` +
67
+ ` • The MDX file is imported without a default import (use: import MyPrompt from "./prompt.mdx")\n\n` +
68
+ `Original error: ${err instanceof Error ? err.message : String(err)}`);
69
+ }
70
+ return result;
71
+ }
72
+ }
73
+ /**
74
+ * @param {unknown} value
75
+ * @returns {value is import("zod").ZodObject<import("zod").ZodRawShape>}
76
+ */
77
+ function isZodObject(value) {
78
+ return Boolean(value && typeof value === "object" && "shape" in value);
79
+ }
80
+ /**
81
+ * @param {DepsSpec | undefined} deps
82
+ * @param {Record<string, string> | undefined} needs
83
+ * @returns {string[] | undefined}
84
+ */
85
+ function deriveDepNodeIds(deps, needs) {
86
+ if (!deps)
87
+ return undefined;
88
+ const ids = new Set();
89
+ for (const key of Object.keys(deps)) {
90
+ const nodeId = needs?.[key] ?? key;
91
+ if (nodeId)
92
+ ids.add(nodeId);
93
+ }
94
+ return ids.size > 0 ? [...ids] : undefined;
95
+ }
96
+ /**
97
+ * @param {string[] | undefined} dependsOn
98
+ * @param {string[] | undefined} depNodeIds
99
+ * @returns {string[] | undefined}
100
+ */
101
+ function mergeDependsOn(dependsOn, depNodeIds) {
102
+ const merged = new Set();
103
+ for (const id of dependsOn ?? [])
104
+ merged.add(id);
105
+ for (const id of depNodeIds ?? [])
106
+ merged.add(id);
107
+ return merged.size > 0 ? [...merged] : undefined;
108
+ }
109
+ /**
110
+ * @param {any} ctx
111
+ * @param {DepsSpec | undefined} deps
112
+ * @param {Record<string, string> | undefined} needs
113
+ * @param {string} [taskId]
114
+ * @returns {Record<string, unknown> | null}
115
+ */
116
+ function resolveDeps(ctx, deps, needs, taskId) {
117
+ if (!deps)
118
+ return Object.create(null);
119
+ const keys = Object.keys(deps);
120
+ if (keys.length === 0)
121
+ return Object.create(null);
122
+ const resolved = Object.create(null);
123
+ for (const key of keys) {
124
+ const target = deps[key];
125
+ const nodeId = needs?.[key] ?? key;
126
+ const value = ctx.outputMaybe(target, { nodeId });
127
+ if (value === undefined)
128
+ return null;
129
+ resolved[key] = value;
130
+ }
131
+ return resolved;
132
+ }
133
+ /**
134
+ * Validate that all deps are satisfied. Throws a descriptive SmithersError
135
+ * naming which dep is missing and which task needs it.
136
+ * @param {{ outputMaybe: (target: unknown, opts: { nodeId: string }) => unknown }} ctx
137
+ * @param {DepsSpec} deps
138
+ * @param {Record<string, string> | undefined} needs
139
+ * @param {string} [taskId]
140
+ * @returns {void}
141
+ */
142
+ function validateDeps(ctx, deps, needs, taskId) {
143
+ for (const key of Object.keys(deps)) {
144
+ const target = deps[key];
145
+ const nodeId = needs?.[key] ?? key;
146
+ const value = ctx.outputMaybe(target, { nodeId });
147
+ if (value === undefined) {
148
+ throw new SmithersError("DEP_NOT_SATISFIED", `Task "${taskId}" dependency "${key}" (resolved from node "${nodeId}") is not satisfied. ` +
149
+ `The upstream task must complete and produce output before this task can run.`, { taskId, depKey: key, resolvedNodeId: nodeId });
150
+ }
151
+ }
152
+ }
153
+ /**
154
+ * @param {AgentLike} agent
155
+ * @param {string[] | undefined} allowTools
156
+ * @returns {AgentLike}
157
+ */
158
+ function applyCliToolAllowlist(agent, allowTools) {
159
+ if (!allowTools) {
160
+ return agent;
161
+ }
162
+ if (agent instanceof ClaudeCodeAgent) {
163
+ const opts = { ...agent.opts };
164
+ if (allowTools.length === 0) {
165
+ return new ClaudeCodeAgent({
166
+ ...opts,
167
+ allowedTools: [],
168
+ tools: "",
169
+ });
170
+ }
171
+ return new ClaudeCodeAgent({
172
+ ...opts,
173
+ allowedTools: [...allowTools],
174
+ });
175
+ }
176
+ if (agent instanceof PiAgent) {
177
+ const opts = { ...agent.opts };
178
+ if (allowTools.length === 0) {
179
+ return new PiAgent({
180
+ ...opts,
181
+ tools: [],
182
+ noTools: true,
183
+ });
184
+ }
185
+ return new PiAgent({
186
+ ...opts,
187
+ tools: [...allowTools],
188
+ noTools: false,
189
+ });
190
+ }
191
+ if (agent instanceof GeminiAgent) {
192
+ const opts = { ...agent.opts };
193
+ return new GeminiAgent({
194
+ ...opts,
195
+ allowedTools: [...allowTools],
196
+ });
197
+ }
198
+ return agent;
199
+ }
200
+ /**
201
+ * @param {unknown} ctx
202
+ * @param {string[] | undefined} allowTools
203
+ * @returns {string[] | undefined}
204
+ */
205
+ function resolveCliToolAllowlist(ctx, allowTools) {
206
+ if (allowTools !== undefined) {
207
+ return allowTools;
208
+ }
209
+ const cliAgentToolsDefault = ctx && typeof ctx === "object"
210
+ ? ctx.__smithersRuntime?.cliAgentToolsDefault
211
+ : undefined;
212
+ return cliAgentToolsDefault === "explicit-only" ? [] : undefined;
213
+ }
214
+ /**
215
+ * @template Row, Output, D
216
+ * @param {TaskProps<Row, Output, D>} props
217
+ * @returns {React.ReactElement | null}
218
+ */
219
+ export function Task(props) {
220
+ const { children, agent, fallbackAgent, deps, ...rest } = props;
221
+ const taskContext = props.smithersContext ?? SmithersContext;
222
+ const ctx = React.useContext(taskContext);
223
+ const aspectCtx = React.useContext(AspectContext);
224
+ const depNodeIds = deriveDepNodeIds(deps, rest.needs);
225
+ if (deps && !ctx) {
226
+ throw new SmithersError("CONTEXT_OUTSIDE_WORKFLOW", "Task deps require a workflow context. Build the workflow with createSmithers().");
227
+ }
228
+ const resolvedDeps = deps ? resolveDeps(ctx, deps, rest.needs, rest.id) : undefined;
229
+ if (deps && resolvedDeps == null) {
230
+ // Deps not yet available — component defers until upstream tasks complete.
231
+ // This is normal reactive behavior; the task will re-render once deps are ready.
232
+ return null;
233
+ }
234
+ // Build aspect metadata to attach to the task element so the engine can
235
+ // enforce budgets and tracking at execution time.
236
+ const aspectMeta = aspectCtx ? buildAspectMeta(aspectCtx) : undefined;
237
+ const agentChain = Array.isArray(agent)
238
+ ? fallbackAgent
239
+ ? [...agent, fallbackAgent]
240
+ : agent
241
+ : agent && fallbackAgent
242
+ ? [agent, fallbackAgent]
243
+ : agent;
244
+ const effectiveAllowTools = resolveCliToolAllowlist(ctx, rest.allowTools);
245
+ const restrictedAgentChain = Array.isArray(agentChain)
246
+ ? agentChain.map((entry) => applyCliToolAllowlist(entry, effectiveAllowTools))
247
+ : agentChain
248
+ ? applyCliToolAllowlist(agentChain, effectiveAllowTools)
249
+ : agentChain;
250
+ const nextDependsOn = mergeDependsOn(rest.dependsOn, depNodeIds);
251
+ const childValue = typeof children === "function" && (agent || deps)
252
+ ? children(resolvedDeps ?? Object.create(null))
253
+ : children;
254
+ if (agent) {
255
+ // Auto-inject `schema` prop into React element children when output is a ZodObject
256
+ let childElement = childValue;
257
+ const schemaForInjection = props.outputSchema ??
258
+ (isZodObject(props.output) ? props.output : undefined);
259
+ if (React.isValidElement(childValue) && schemaForInjection) {
260
+ childElement = React.cloneElement(childValue, {
261
+ schema: zodSchemaToJsonExample(schemaForInjection),
262
+ });
263
+ }
264
+ const prompt = renderPromptToText(childElement);
265
+ return React.createElement("smithers:task", {
266
+ ...rest,
267
+ dependsOn: nextDependsOn,
268
+ waitAsync: rest.async === true,
269
+ agent: restrictedAgentChain,
270
+ __smithersKind: "agent",
271
+ ...aspectMeta,
272
+ }, prompt);
273
+ }
274
+ if (typeof children === "function" && !deps) {
275
+ const nextProps = {
276
+ ...rest,
277
+ dependsOn: nextDependsOn,
278
+ waitAsync: rest.async === true,
279
+ __smithersKind: "compute",
280
+ __smithersComputeFn: children,
281
+ ...aspectMeta,
282
+ };
283
+ return React.createElement("smithers:task", nextProps, null);
284
+ }
285
+ const nextProps = {
286
+ ...rest,
287
+ dependsOn: nextDependsOn,
288
+ waitAsync: rest.async === true,
289
+ __smithersKind: "static",
290
+ __smithersPayload: childValue,
291
+ __payload: childValue,
292
+ ...aspectMeta,
293
+ };
294
+ return React.createElement("smithers:task", nextProps, null);
295
+ }
296
+ /**
297
+ * Build the __aspects metadata object from the current AspectContext.
298
+ * This is attached to the smithers:task element props so the engine
299
+ * can read budgets and tracking config at execution time.
300
+ * @param {{
301
+ * tokenBudget?: unknown;
302
+ * latencySlo?: unknown;
303
+ * costBudget?: unknown;
304
+ * tracking?: unknown;
305
+ * accumulator?: unknown;
306
+ * }} aspectCtx
307
+ * @returns {{ __aspects: Record<string, unknown> }}
308
+ */
309
+ function buildAspectMeta(aspectCtx) {
310
+ return {
311
+ __aspects: {
312
+ tokenBudget: aspectCtx.tokenBudget,
313
+ latencySlo: aspectCtx.latencySlo,
314
+ costBudget: aspectCtx.costBudget,
315
+ tracking: aspectCtx.tracking,
316
+ accumulator: aspectCtx.accumulator,
317
+ },
318
+ };
319
+ }
@@ -0,0 +1,57 @@
1
+ import type React from "react";
2
+ import type { z } from "zod";
3
+ import type { AgentLike } from "@smithers-orchestrator/agents/AgentLike";
4
+ import type { SmithersCtx } from "@smithers-orchestrator/driver";
5
+ import type { CachePolicy } from "@smithers-orchestrator/scheduler/CachePolicy";
6
+ import type { RetryPolicy } from "@smithers-orchestrator/scheduler/RetryPolicy";
7
+ import type { ScorersMap } from "@smithers-orchestrator/scorers/types";
8
+ import type { TaskMemoryConfig } from "@smithers-orchestrator/memory/types";
9
+ import type { OutputTarget } from "./OutputTarget.ts";
10
+ import type { DepsSpec } from "./DepsSpec.ts";
11
+ import type { InferDeps } from "./InferDeps.ts";
12
+
13
+ export type TaskProps<Row, Output extends OutputTarget = OutputTarget, D extends DepsSpec = {}> = {
14
+ key?: string;
15
+ id: string;
16
+ /** Where to store the task's result. Pass a Zod schema from `outputs` (recommended), a Drizzle table, or a string key. */
17
+ output: Output;
18
+ /**
19
+ * Optional Zod schema describing the expected agent output shape.
20
+ * When `output` is already a ZodObject this is inferred automatically.
21
+ * Used for validation and to inject schema examples into MDX prompts.
22
+ */
23
+ outputSchema?: z.ZodObject<z.ZodRawShape>;
24
+ /** Agent or array of agents [primary, fallback1, fallback2, ...]. Tries in order on retries. */
25
+ agent?: AgentLike | AgentLike[];
26
+ /** Convenience alias for a single retry fallback without exposing array syntax in JSX. */
27
+ fallbackAgent?: AgentLike;
28
+ /** Explicit dependency on other task node IDs. The task will not run until all listed tasks complete. */
29
+ dependsOn?: string[];
30
+ /** Named dependencies on other tasks. Keys become context keys, values are task node IDs. */
31
+ needs?: Record<string, string>;
32
+ /** Render-time typed dependencies. Keys resolve from task ids of the same name, or from matching `needs` entries. */
33
+ deps?: D;
34
+ skipIf?: boolean;
35
+ needsApproval?: boolean;
36
+ /** When paired with `needsApproval`, do not block unrelated downstream flow while the approval is pending. */
37
+ async?: boolean;
38
+ timeoutMs?: number;
39
+ heartbeatTimeoutMs?: number;
40
+ heartbeatTimeout?: number;
41
+ /** Disable retries entirely. Equivalent to retries={0}. */
42
+ noRetry?: boolean;
43
+ retries?: number;
44
+ retryPolicy?: RetryPolicy;
45
+ continueOnFail?: boolean;
46
+ cache?: CachePolicy;
47
+ /** Optional scorers to evaluate this task's output after completion. */
48
+ scorers?: ScorersMap;
49
+ /** Optional cross-run memory configuration. */
50
+ memory?: TaskMemoryConfig;
51
+ allowTools?: string[];
52
+ label?: string;
53
+ meta?: Record<string, unknown>;
54
+ /** @internal Used by createSmithers() to bind tasks to the correct workflow context. */
55
+ smithersContext?: React.Context<SmithersCtx<unknown> | null>;
56
+ children?: string | Row | (() => Row | Promise<Row>) | React.ReactNode | ((deps: InferDeps<D>) => Row | React.ReactNode);
57
+ };
@@ -0,0 +1,42 @@
1
+ import React from "react";
2
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
3
+ /** @typedef {import("./TimerProps.ts").TimerProps} TimerProps */
4
+
5
+ /**
6
+ * @param {TimerProps} props
7
+ */
8
+ export function Timer(props) {
9
+ if (props.skipIf)
10
+ return null;
11
+ const hasDuration = typeof props.duration === "string" && props.duration.trim().length > 0;
12
+ const hasUntil = props.until !== undefined && props.until !== null && String(props.until).trim().length > 0;
13
+ if ((hasDuration ? 1 : 0) + (hasUntil ? 1 : 0) !== 1) {
14
+ throw new SmithersError("INVALID_INPUT", `<Timer id="${props.id}"> requires exactly one of "duration" or "until".`);
15
+ }
16
+ if (props.every !== undefined) {
17
+ throw new SmithersError("INVALID_INPUT", `<Timer id="${props.id}"> does not support "every" yet. Recurring timers ship in phase 2.`);
18
+ }
19
+ const untilIso = props.until instanceof Date
20
+ ? props.until.toISOString()
21
+ : typeof props.until === "string"
22
+ ? props.until
23
+ : undefined;
24
+ const timerMeta = {
25
+ timer: true,
26
+ ...(hasDuration ? { duration: props.duration } : {}),
27
+ ...(hasUntil ? { until: untilIso } : {}),
28
+ ...props.meta,
29
+ };
30
+ return React.createElement("smithers:timer", {
31
+ id: props.id,
32
+ key: props.key,
33
+ duration: props.duration,
34
+ until: untilIso,
35
+ dependsOn: props.dependsOn,
36
+ needs: props.needs,
37
+ label: props.label ?? `timer:${props.id}`,
38
+ meta: Object.keys(timerMeta).length > 0 ? timerMeta : undefined,
39
+ __smithersTimerDuration: props.duration,
40
+ __smithersTimerUntil: untilIso,
41
+ });
42
+ }
@@ -0,0 +1,21 @@
1
+ export type TimerProps = {
2
+ id: string;
3
+ /**
4
+ * Relative duration (examples: "500ms", "1s", "30m", "1h", "7d").
5
+ */
6
+ duration?: string;
7
+ /**
8
+ * Absolute fire time (ISO timestamp or Date).
9
+ */
10
+ until?: string | Date;
11
+ /**
12
+ * Recurring timer syntax is reserved for phase 2 and is not supported yet.
13
+ */
14
+ every?: string;
15
+ skipIf?: boolean;
16
+ dependsOn?: string[];
17
+ needs?: Record<string, string>;
18
+ label?: string;
19
+ meta?: Record<string, unknown>;
20
+ key?: string;
21
+ };
@@ -0,0 +1,35 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./TryCatchFinallyProps.ts").TryCatchFinallyProps} TryCatchFinallyProps */
3
+ // @smithers-type-exports-end
4
+
5
+ import React from "react";
6
+ import { forceContinueOnFail } from "./control-flow-utils.js";
7
+ /**
8
+ * Workflow-scoped error boundary. Catch specific error types, run recovery
9
+ * handlers, and ensure cleanup always runs.
10
+ *
11
+ * - The `try` block is the main workflow content.
12
+ * - If any task in `try` fails with a matching error, the `catch` block mounts.
13
+ * - The `finally` block always runs after try (success) or catch (failure).
14
+ *
15
+ * Renders to `<smithers:try-catch-finally>`.
16
+ * @param {TryCatchFinallyProps} props
17
+ */
18
+ export function TryCatchFinally(props) {
19
+ if (props.skipIf)
20
+ return null;
21
+ const { id, catch: catchHandler, catchErrors, finally: finallyHandler } = props;
22
+ const tryBlock = forceContinueOnFail(props.try);
23
+ const catchBlock = catchHandler && typeof catchHandler !== "function" ? catchHandler : null;
24
+ const hostProps = {
25
+ id,
26
+ __tcfCatchErrors: catchErrors,
27
+ __tcfCatchHandler: catchHandler,
28
+ __tcfFinallyHandler: finallyHandler,
29
+ };
30
+ return React.createElement("smithers:try-catch-finally", hostProps, React.createElement("smithers:tcf-try", null, tryBlock), catchBlock
31
+ ? React.createElement("smithers:tcf-catch", null, catchBlock)
32
+ : null, finallyHandler
33
+ ? React.createElement("smithers:tcf-finally", null, finallyHandler)
34
+ : null);
35
+ }
@@ -0,0 +1,12 @@
1
+ import type React from "react";
2
+ import type { SmithersErrorCode } from "@smithers-orchestrator/errors/SmithersErrorCode";
3
+ import type { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
4
+
5
+ export type TryCatchFinallyProps = {
6
+ id?: string;
7
+ try: React.ReactElement;
8
+ catch?: React.ReactElement | ((error: SmithersError) => React.ReactElement);
9
+ catchErrors?: SmithersErrorCode[];
10
+ finally?: React.ReactElement;
11
+ skipIf?: boolean;
12
+ };
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ import { getTaskRuntime } from "@smithers-orchestrator/driver/task-runtime";
3
+ import { SmithersDb } from "@smithers-orchestrator/db/adapter";
4
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
5
+ /** @typedef {import("./WaitForEventProps.ts").WaitForEventProps} WaitForEventProps */
6
+
7
+ /**
8
+ * @param {WaitForEventProps} props
9
+ */
10
+ export function WaitForEvent(props) {
11
+ if (props.skipIf)
12
+ return null;
13
+ const eventMeta = {
14
+ event: props.event,
15
+ ...(props.correlationId ? { correlationId: props.correlationId } : {}),
16
+ ...(props.onTimeout ? { onTimeout: props.onTimeout } : {}),
17
+ ...props.meta,
18
+ };
19
+ return React.createElement("smithers:wait-for-event", {
20
+ id: props.id,
21
+ key: props.key,
22
+ event: props.event,
23
+ correlationId: props.correlationId,
24
+ output: props.output,
25
+ outputSchema: props.outputSchema,
26
+ timeoutMs: props.timeoutMs,
27
+ onTimeout: props.onTimeout ?? "fail",
28
+ waitAsync: props.async === true,
29
+ dependsOn: props.dependsOn,
30
+ needs: props.needs,
31
+ label: props.label ?? `wait:${props.event}`,
32
+ meta: Object.keys(eventMeta).length > 0 ? eventMeta : undefined,
33
+ __smithersEventName: props.event,
34
+ __smithersCorrelationId: props.correlationId,
35
+ __smithersOnTimeout: props.onTimeout ?? "fail",
36
+ });
37
+ }
@@ -0,0 +1,28 @@
1
+ import type { z } from "zod";
2
+ import type { OutputTarget } from "./OutputTarget.ts";
3
+
4
+ export type WaitForEventProps = {
5
+ id: string;
6
+ /** Event name/type to wait for. */
7
+ event: string;
8
+ /** Correlation key to match the right event instance. */
9
+ correlationId?: string;
10
+ /** Where to store the event payload. */
11
+ output: OutputTarget;
12
+ /** Zod schema for the event payload. */
13
+ outputSchema?: z.ZodObject<z.ZodRawShape>;
14
+ /** Max wait time in ms before timing out. */
15
+ timeoutMs?: number;
16
+ /** Behavior on timeout: fail (default), skip the node, or continue with null. */
17
+ onTimeout?: "fail" | "skip" | "continue";
18
+ /** Do not block unrelated downstream flow while waiting for the event. */
19
+ async?: boolean;
20
+ skipIf?: boolean;
21
+ /** Explicit dependency on other task node IDs. */
22
+ dependsOn?: string[];
23
+ /** Named dependencies on other tasks. Keys become context keys, values are task node IDs. */
24
+ needs?: Record<string, string>;
25
+ label?: string;
26
+ meta?: Record<string, unknown>;
27
+ key?: string;
28
+ };
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ /** @typedef {import("./WorkflowProps.ts").WorkflowProps} WorkflowProps */
3
+
4
+ /**
5
+ * @param {WorkflowProps} props
6
+ * @returns {React.DOMElement<WorkflowProps, Element>}
7
+ */
8
+ export function Workflow(props) {
9
+ return React.createElement("smithers:workflow", props, props.children);
10
+ }
@@ -0,0 +1,7 @@
1
+ import type React from "react";
2
+
3
+ export type WorkflowProps = {
4
+ name: string;
5
+ cache?: boolean;
6
+ children?: React.ReactNode;
7
+ };
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+ import { WORKTREE_EMPTY_PATH_ERROR } from "@smithers-orchestrator/graph/constants";
3
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
4
+ /** @typedef {import("./WorktreeProps.ts").WorktreeProps} WorktreeProps */
5
+
6
+ /**
7
+ * @param {WorktreeProps} props
8
+ */
9
+ export function Worktree(props) {
10
+ if (typeof props.path !== "string" || props.path.trim() === "") {
11
+ throw new SmithersError("WORKTREE_EMPTY_PATH", WORKTREE_EMPTY_PATH_ERROR);
12
+ }
13
+ if (props.skipIf)
14
+ return null;
15
+ const next = { id: props.id, path: props.path, branch: props.branch, baseBranch: props.baseBranch };
16
+ return React.createElement("smithers:worktree", next, props.children);
17
+ }
@@ -0,0 +1,11 @@
1
+ import type React from "react";
2
+
3
+ export type WorktreeProps = {
4
+ id?: string;
5
+ path: string;
6
+ branch?: string;
7
+ /** Base branch for syncing worktrees (default: "main"). */
8
+ baseBranch?: string;
9
+ skipIf?: boolean;
10
+ children?: React.ReactNode;
11
+ };
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ /**
3
+ * @param {unknown} node
4
+ * @returns {unknown}
5
+ */
6
+ function mapChildren(node) {
7
+ if (Array.isArray(node)) {
8
+ return node.map((child) => forceContinueOnFail(child));
9
+ }
10
+ if (React.isValidElement(node)) {
11
+ return forceContinueOnFail(node);
12
+ }
13
+ return node;
14
+ }
15
+ /**
16
+ * Failure-boundary components need inner tasks to fail "softly" so the
17
+ * scheduler can decide whether to run catch/finally or compensations.
18
+ * @param {React.ReactNode} node
19
+ * @returns {React.ReactNode}
20
+ */
21
+ export function forceContinueOnFail(node) {
22
+ if (!React.isValidElement(node)) {
23
+ return node;
24
+ }
25
+ const props = (node.props ?? {});
26
+ const nextProps = {};
27
+ if ("output" in props) {
28
+ nextProps.continueOnFail = true;
29
+ }
30
+ if ("children" in props) {
31
+ nextProps.children = mapChildren(props.children);
32
+ }
33
+ if (Object.keys(nextProps).length === 0) {
34
+ return node;
35
+ }
36
+ return React.cloneElement(node, nextProps);
37
+ }