@intentius/chant 0.1.5 → 0.1.7

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.
@@ -6,8 +6,6 @@ import { build } from "../../build";
6
6
  import { computeBuildDigest, diffDigests } from "../../state/digest";
7
7
  import { takeSnapshot } from "../../state/snapshot";
8
8
  import type { StateSnapshot } from "../../state/types";
9
- import { discoverSpells } from "../../spell/discovery";
10
-
11
9
  export interface ToolRegistration {
12
10
  definition: ToolDefinition;
13
11
  handler: ToolHandler;
@@ -87,52 +85,3 @@ export function createDiffTool(plugins: LexiconPlugin[]): ToolRegistration {
87
85
  };
88
86
  }
89
87
 
90
- /**
91
- * Create spell-done tool definition and handler
92
- */
93
- export function createSpellDoneTool(): ToolRegistration {
94
- return {
95
- definition: {
96
- name: "spell-done",
97
- description: "Mark a spell task as done",
98
- inputSchema: {
99
- type: "object",
100
- properties: {
101
- name: { type: "string", description: "Spell name" },
102
- taskNumber: { type: "number", description: "Task number (1-based)" },
103
- },
104
- required: ["name", "taskNumber"],
105
- },
106
- },
107
- handler: async (params) => {
108
- const { readFileSync, writeFileSync } = await import("node:fs");
109
- const { spells } = await discoverSpells();
110
- const name = params.name as string;
111
- const taskNumber = params.taskNumber as number;
112
- const spell = spells.get(name);
113
- if (!spell) return `Spell "${name}" not found`;
114
- if (taskNumber < 1 || taskNumber > spell.definition.tasks.length) {
115
- return `Invalid task number ${taskNumber}`;
116
- }
117
- const task = spell.definition.tasks[taskNumber - 1];
118
- if (task.done) return `Task ${taskNumber} is already done`;
119
-
120
- const content = readFileSync(spell.filePath, "utf-8");
121
- let count = 0;
122
- const rewritten = content.replace(
123
- /task\(("[^"]*"|'[^']*'|`[^`]*`)((?:\s*,\s*\{[^}]*\})?)\)/g,
124
- (match, desc, opts) => {
125
- count++;
126
- if (count !== taskNumber) return match;
127
- if (opts && opts.includes("done:")) {
128
- return match.replace(/done:\s*false/, "done: true");
129
- }
130
- return `task(${desc}, { done: true })`;
131
- },
132
- );
133
- if (rewritten === content) return `Could not rewrite task ${taskNumber}`;
134
- writeFileSync(spell.filePath, rewritten);
135
- return `Task ${taskNumber} marked done: "${task.description}"`;
136
- },
137
- };
138
- }
@@ -18,6 +18,8 @@ export interface ParsedArgs {
18
18
  watch: boolean;
19
19
  verbose: boolean;
20
20
  help: boolean;
21
+ profile?: string;
22
+ report?: boolean;
21
23
  }
22
24
 
23
25
  /**
package/src/composite.ts CHANGED
@@ -95,11 +95,16 @@ export function Composite<P, M extends CompositeMembers = CompositeMembers>(
95
95
  }
96
96
  }
97
97
 
98
- const instance: CompositeInstance<M> = {
99
- [COMPOSITE_MARKER]: true,
100
- members,
101
- _definition: definition,
102
- };
98
+ // Define `members` and `_definition` as non-enumerable so spreading a
99
+ // composite instance (`...someComposite`) only exposes the actual member
100
+ // resources, not the framework's bookkeeping properties. Without this, a
101
+ // parent composite that does `...childResult` ends up with a `members` key
102
+ // pointing at the child's CompositeMembers record — not a Declarable —
103
+ // which then trips the parent's own member validation.
104
+ const instance = {} as CompositeInstance<M>;
105
+ Object.defineProperty(instance, COMPOSITE_MARKER, { value: true, enumerable: false });
106
+ Object.defineProperty(instance, "members", { value: members, enumerable: false });
107
+ Object.defineProperty(instance, "_definition", { value: definition, enumerable: false });
103
108
 
104
109
  return Object.assign(instance, members) as CompositeInstance<M> & M;
105
110
  }) as CompositeDefinition<P, M>;
package/src/index.ts CHANGED
@@ -60,4 +60,7 @@ export * from "./lsp/types";
60
60
  export * from "./lsp/lexicon-providers";
61
61
  export * from "./mcp/types";
62
62
  export * from "./state/index";
63
- export * from "./spell/index";
63
+ // Op builders — use explicit exports to avoid collision with the core `build` function
64
+ export { Op, phase, activity, gate, kubectlApply, helmInstall, waitForStack,
65
+ gitlabPipeline, stateSnapshot, shell, teardown, OpResource } from "./op/index";
66
+ export type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep } from "./op/index";
@@ -0,0 +1,96 @@
1
+ import { OpResource } from "./resource";
2
+ import type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep } from "./types";
3
+
4
+ // ── Core builders ─────────────────────────────────────────────────────────────
5
+
6
+ /**
7
+ * Declare a named, phased Temporal workflow.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * export default Op({
12
+ * name: "alb-deploy",
13
+ * overview: "Build and deploy the ALB multi-service stack",
14
+ * phases: [
15
+ * phase("Build", [build("examples/gitlab-aws-alb-infra")], { parallel: true }),
16
+ * phase("Deploy", [kubectlApply("dist/alb-infra.yaml")]),
17
+ * ],
18
+ * });
19
+ * ```
20
+ */
21
+ export function Op(config: OpConfig): InstanceType<typeof OpResource> {
22
+ return new OpResource(config as unknown as Record<string, unknown>);
23
+ }
24
+
25
+ /** Define a named execution phase containing one or more steps. */
26
+ export function phase(
27
+ name: string,
28
+ steps: StepDefinition[],
29
+ opts?: { parallel?: boolean },
30
+ ): PhaseDefinition {
31
+ return { name, steps, ...(opts?.parallel ? { parallel: true } : {}) };
32
+ }
33
+
34
+ /** Reference a pre-built or custom activity by function name. */
35
+ export function activity(
36
+ fn: string,
37
+ args?: Record<string, unknown>,
38
+ profile?: ActivityStep["profile"],
39
+ ): ActivityStep {
40
+ return {
41
+ kind: "activity",
42
+ fn,
43
+ ...(args && Object.keys(args).length > 0 ? { args } : {}),
44
+ ...(profile ? { profile } : {}),
45
+ };
46
+ }
47
+
48
+ /** Insert a human gate — the workflow pauses until the named signal is received. */
49
+ export function gate(
50
+ signalName: string,
51
+ opts?: { timeout?: string; description?: string },
52
+ ): GateStep {
53
+ return {
54
+ kind: "gate",
55
+ signalName,
56
+ ...(opts?.timeout ? { timeout: opts.timeout } : {}),
57
+ ...(opts?.description ? { description: opts.description } : {}),
58
+ };
59
+ }
60
+
61
+ // ── Pre-built activity shortcuts ──────────────────────────────────────────────
62
+
63
+ /** Run `npm run build` (or `chant build`) in the given project directory. */
64
+ export const build = (path: string, opts?: Record<string, unknown>): ActivityStep =>
65
+ activity("chantBuild", { path, ...opts });
66
+
67
+ /** Run `kubectl apply -f <manifest>`. Uses `longInfra` profile. */
68
+ export const kubectlApply = (manifest: string, opts?: Record<string, unknown>): ActivityStep =>
69
+ activity("kubectlApply", { manifest, ...opts }, "longInfra");
70
+
71
+ /** Run `helm upgrade --install`. Uses `longInfra` profile. */
72
+ export const helmInstall = (
73
+ name: string,
74
+ chart: string,
75
+ opts?: { values?: string; namespace?: string; [k: string]: unknown },
76
+ ): ActivityStep => activity("helmInstall", { name, chart, ...opts }, "longInfra");
77
+
78
+ /** Poll for stack readiness (kubectl rollout, CloudFormation complete, etc). Uses `k8sWait` profile. */
79
+ export const waitForStack = (name: string, opts?: Record<string, unknown>): ActivityStep =>
80
+ activity("waitForStack", { name, ...opts }, "k8sWait");
81
+
82
+ /** Trigger and wait for a GitLab CI pipeline to complete. Uses `longInfra` profile. */
83
+ export const gitlabPipeline = (name: string, opts?: Record<string, unknown>): ActivityStep =>
84
+ activity("gitlabPipeline", { name, ...opts }, "longInfra");
85
+
86
+ /** Take a chant state snapshot for the given environment. */
87
+ export const stateSnapshot = (env: string): ActivityStep =>
88
+ activity("stateSnapshot", { env });
89
+
90
+ /** Run an arbitrary shell command. */
91
+ export const shell = (cmd: string, opts?: { env?: Record<string, string> }): ActivityStep =>
92
+ activity("shellCmd", { cmd, ...opts });
93
+
94
+ /** Run `chant teardown` in the given project directory. Uses `longInfra` profile. */
95
+ export const teardown = (path: string): ActivityStep =>
96
+ activity("chantTeardown", { path }, "longInfra");
@@ -0,0 +1,43 @@
1
+ import { describe, test, expect, vi, beforeEach } from "vitest";
2
+ import { discoverOps } from "./discover";
3
+
4
+ // Mock getRuntime to return git root pointing at the repo root
5
+ vi.mock("../runtime-adapter", () => ({
6
+ getRuntime: () => ({
7
+ spawn: async (cmd: string[]) => {
8
+ if (cmd[0] === "git" && cmd[1] === "rev-parse") {
9
+ // Return the actual repo root so the test can find the example op file
10
+ const { execFile } = await import("node:child_process");
11
+ const { promisify } = await import("node:util");
12
+ const execFileAsync = promisify(execFile);
13
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"]);
14
+ return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
15
+ }
16
+ return { stdout: "", stderr: "", exitCode: 0 };
17
+ },
18
+ }),
19
+ }));
20
+
21
+ describe("discoverOps", () => {
22
+ test("discovers alb-deploy.op.ts from examples/", async () => {
23
+ const { ops, errors } = await discoverOps();
24
+ expect(errors).toHaveLength(0);
25
+ expect(ops.has("alb-deploy")).toBe(true);
26
+ });
27
+
28
+ test("discovered Op has correct config shape", async () => {
29
+ const { ops } = await discoverOps();
30
+ const op = ops.get("alb-deploy");
31
+ expect(op).toBeDefined();
32
+ expect(op!.config.name).toBe("alb-deploy");
33
+ expect(Array.isArray(op!.config.phases)).toBe(true);
34
+ expect(op!.config.phases.length).toBeGreaterThan(0);
35
+ expect(typeof op!.config.overview).toBe("string");
36
+ });
37
+
38
+ test("filePath points to the .op.ts source file", async () => {
39
+ const { ops } = await discoverOps();
40
+ const op = ops.get("alb-deploy");
41
+ expect(op!.filePath).toMatch(/alb-deploy\.op\.ts$/);
42
+ });
43
+ });
@@ -0,0 +1,89 @@
1
+ import { getRuntime } from "../runtime-adapter";
2
+ import { readdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import type { OpConfig } from "./types";
5
+
6
+ export interface DiscoveredOp {
7
+ config: OpConfig;
8
+ filePath: string;
9
+ }
10
+
11
+ export interface OpDiscoveryResult {
12
+ ops: Map<string, DiscoveredOp>;
13
+ errors: string[];
14
+ }
15
+
16
+ async function findGitRoot(cwd?: string): Promise<string> {
17
+ const rt = getRuntime();
18
+ const result = await rt.spawn(["git", "rev-parse", "--show-toplevel"], { cwd });
19
+ if (result.exitCode !== 0) throw new Error("Not in a git repository");
20
+ return result.stdout.trim();
21
+ }
22
+
23
+ async function collectOpFiles(dir: string): Promise<string[]> {
24
+ const files: string[] = [];
25
+ let entries;
26
+ try {
27
+ entries = await readdir(dir, { withFileTypes: true });
28
+ } catch {
29
+ return files;
30
+ }
31
+ for (const entry of entries) {
32
+ const fullPath = join(dir, entry.name);
33
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules" && entry.name !== "dist") {
34
+ files.push(...await collectOpFiles(fullPath));
35
+ } else if (
36
+ entry.isFile() &&
37
+ entry.name.endsWith(".op.ts") &&
38
+ !entry.name.endsWith(".test.ts") &&
39
+ !entry.name.endsWith(".spec.ts")
40
+ ) {
41
+ files.push(fullPath);
42
+ }
43
+ }
44
+ return files;
45
+ }
46
+
47
+ /**
48
+ * Discover all Op definitions from *.op.ts files under the git root.
49
+ */
50
+ export async function discoverOps(opts?: { cwd?: string }): Promise<OpDiscoveryResult> {
51
+ const errors: string[] = [];
52
+ const ops = new Map<string, DiscoveredOp>();
53
+
54
+ const gitRoot = await findGitRoot(opts?.cwd);
55
+ const files = await collectOpFiles(gitRoot);
56
+
57
+ const nameToFile = new Map<string, string>();
58
+
59
+ for (const filePath of files) {
60
+ try {
61
+ const mod = await import(filePath);
62
+ const entity = mod.default;
63
+
64
+ if (!entity || typeof entity !== "object") {
65
+ errors.push(`${filePath}: default export is not an object`);
66
+ continue;
67
+ }
68
+
69
+ const config = entity.props as OpConfig | undefined;
70
+
71
+ if (!config || typeof config.name !== "string" || !Array.isArray(config.phases)) {
72
+ errors.push(`${filePath}: default export is not a valid Op (missing name or phases)`);
73
+ continue;
74
+ }
75
+
76
+ if (nameToFile.has(config.name)) {
77
+ errors.push(`Duplicate Op name "${config.name}" in ${filePath} and ${nameToFile.get(config.name)}`);
78
+ continue;
79
+ }
80
+
81
+ nameToFile.set(config.name, filePath);
82
+ ops.set(config.name, { config, filePath });
83
+ } catch (err) {
84
+ errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
85
+ }
86
+ }
87
+
88
+ return { ops, errors };
89
+ }
@@ -0,0 +1,6 @@
1
+ export { Op, phase, activity, gate, build, kubectlApply, helmInstall, waitForStack,
2
+ gitlabPipeline, stateSnapshot, shell, teardown } from "./builders";
3
+ export { OpResource } from "./resource";
4
+ export type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep } from "./types";
5
+ export { discoverOps } from "./discover";
6
+ export type { DiscoveredOp, OpDiscoveryResult } from "./discover";
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Core Op builder tests.
3
+ */
4
+
5
+ import { describe, expect, it } from "vitest";
6
+ import { Op, phase, activity, gate, build, kubectlApply, helmInstall,
7
+ waitForStack, gitlabPipeline, stateSnapshot, shell, teardown } from "./builders";
8
+ import { DECLARABLE_MARKER, type Declarable } from "../declarable";
9
+
10
+ // ── Op() ──────────────────────────────────────────────────────────────────────
11
+
12
+ describe("Op()", () => {
13
+ function opProps(op: Declarable): Record<string, unknown> {
14
+ return (op as unknown as { props: Record<string, unknown> }).props;
15
+ }
16
+
17
+ it("returns a Declarable with correct lexicon and entityType", () => {
18
+ const op = Op({ name: "my-op", overview: "Test op", phases: [] });
19
+ expect(op[DECLARABLE_MARKER]).toBe(true);
20
+ expect(op.lexicon).toBe("temporal");
21
+ expect(op.entityType).toBe("Temporal::Op");
22
+ expect(op.kind).toBe("resource");
23
+ });
24
+
25
+ it("stores name and overview in props", () => {
26
+ const op = Op({ name: "deploy-op", overview: "Deploy infra", phases: [] });
27
+ const props = opProps(op);
28
+ expect(props.name).toBe("deploy-op");
29
+ expect(props.overview).toBe("Deploy infra");
30
+ });
31
+
32
+ it("stores phases in props", () => {
33
+ const p = phase("Build", [build("path/to/project")]);
34
+ const op = Op({ name: "op", overview: "o", phases: [p] });
35
+ const props = opProps(op);
36
+ expect(Array.isArray(props.phases)).toBe(true);
37
+ expect((props.phases as unknown[]).length).toBe(1);
38
+ });
39
+
40
+ it("stores optional depends in props", () => {
41
+ const op = Op({ name: "second-op", overview: "o", phases: [], depends: ["first-op"] });
42
+ const props = opProps(op);
43
+ expect(props.depends).toEqual(["first-op"]);
44
+ });
45
+
46
+ it("stores optional onFailure in props", () => {
47
+ const compensation = phase("Rollback", [shell("echo rollback")]);
48
+ const op = Op({ name: "op", overview: "o", phases: [], onFailure: [compensation] });
49
+ const props = opProps(op);
50
+ expect(Array.isArray(props.onFailure)).toBe(true);
51
+ });
52
+
53
+ it("stores optional taskQueue in props", () => {
54
+ const op = Op({ name: "op", overview: "o", phases: [], taskQueue: "custom-queue" });
55
+ const props = opProps(op);
56
+ expect(props.taskQueue).toBe("custom-queue");
57
+ });
58
+ });
59
+
60
+ // ── phase() ───────────────────────────────────────────────────────────────────
61
+
62
+ describe("phase()", () => {
63
+ it("returns correct shape with name and steps", () => {
64
+ const p = phase("Deploy", [build("./")]);
65
+ expect(p.name).toBe("Deploy");
66
+ expect(p.steps).toHaveLength(1);
67
+ });
68
+
69
+ it("sets parallel: true when passed as option", () => {
70
+ const p = phase("Build", [build("a"), build("b")], { parallel: true });
71
+ expect(p.parallel).toBe(true);
72
+ });
73
+
74
+ it("omits parallel key when not set", () => {
75
+ const p = phase("Build", [build("a")]);
76
+ expect("parallel" in p).toBe(false);
77
+ });
78
+ });
79
+
80
+ // ── activity() ────────────────────────────────────────────────────────────────
81
+
82
+ describe("activity()", () => {
83
+ it("returns ActivityStep with kind 'activity'", () => {
84
+ const a = activity("myFn");
85
+ expect(a.kind).toBe("activity");
86
+ expect(a.fn).toBe("myFn");
87
+ });
88
+
89
+ it("includes args when provided", () => {
90
+ const a = activity("myFn", { key: "val" });
91
+ expect(a.args).toEqual({ key: "val" });
92
+ });
93
+
94
+ it("omits args key when no args provided", () => {
95
+ const a = activity("myFn");
96
+ expect("args" in a).toBe(false);
97
+ });
98
+
99
+ it("includes profile when provided", () => {
100
+ const a = activity("myFn", {}, "longInfra");
101
+ expect(a.profile).toBe("longInfra");
102
+ });
103
+
104
+ it("omits profile key when not provided", () => {
105
+ const a = activity("myFn");
106
+ expect("profile" in a).toBe(false);
107
+ });
108
+ });
109
+
110
+ // ── gate() ────────────────────────────────────────────────────────────────────
111
+
112
+ describe("gate()", () => {
113
+ it("returns GateStep with kind 'gate' and signalName", () => {
114
+ const g = gate("dns-delegation");
115
+ expect(g.kind).toBe("gate");
116
+ expect(g.signalName).toBe("dns-delegation");
117
+ });
118
+
119
+ it("includes timeout when provided", () => {
120
+ const g = gate("approval", { timeout: "24h" });
121
+ expect(g.timeout).toBe("24h");
122
+ });
123
+
124
+ it("includes description when provided", () => {
125
+ const g = gate("approval", { description: "Awaiting DNS delegation" });
126
+ expect(g.description).toBe("Awaiting DNS delegation");
127
+ });
128
+
129
+ it("omits timeout and description when not provided", () => {
130
+ const g = gate("sig");
131
+ expect("timeout" in g).toBe(false);
132
+ expect("description" in g).toBe(false);
133
+ });
134
+ });
135
+
136
+ // ── Pre-built shortcuts ───────────────────────────────────────────────────────
137
+
138
+ describe("pre-built shortcuts", () => {
139
+ it("build() produces chantBuild activity with path arg", () => {
140
+ const a = build("./my-project");
141
+ expect(a.kind).toBe("activity");
142
+ expect(a.fn).toBe("chantBuild");
143
+ expect(a.args?.path).toBe("./my-project");
144
+ });
145
+
146
+ it("build() uses default fastIdempotent profile (no profile key)", () => {
147
+ const a = build("./p");
148
+ expect("profile" in a).toBe(false);
149
+ });
150
+
151
+ it("kubectlApply() produces kubectlApply activity with longInfra profile", () => {
152
+ const a = kubectlApply("dist/infra.yaml");
153
+ expect(a.fn).toBe("kubectlApply");
154
+ expect(a.args?.manifest).toBe("dist/infra.yaml");
155
+ expect(a.profile).toBe("longInfra");
156
+ });
157
+
158
+ it("helmInstall() produces helmInstall activity with name and chart", () => {
159
+ const a = helmInstall("my-release", "charts/app");
160
+ expect(a.fn).toBe("helmInstall");
161
+ expect(a.args?.name).toBe("my-release");
162
+ expect(a.args?.chart).toBe("charts/app");
163
+ expect(a.profile).toBe("longInfra");
164
+ });
165
+
166
+ it("waitForStack() produces waitForStack activity with k8sWait profile", () => {
167
+ const a = waitForStack("my-stack");
168
+ expect(a.fn).toBe("waitForStack");
169
+ expect(a.args?.name).toBe("my-stack");
170
+ expect(a.profile).toBe("k8sWait");
171
+ });
172
+
173
+ it("gitlabPipeline() produces gitlabPipeline activity with longInfra profile", () => {
174
+ const a = gitlabPipeline("group/project");
175
+ expect(a.fn).toBe("gitlabPipeline");
176
+ expect(a.args?.name).toBe("group/project");
177
+ expect(a.profile).toBe("longInfra");
178
+ });
179
+
180
+ it("stateSnapshot() produces stateSnapshot activity with env arg", () => {
181
+ const a = stateSnapshot("prod");
182
+ expect(a.fn).toBe("stateSnapshot");
183
+ expect(a.args?.env).toBe("prod");
184
+ expect("profile" in a).toBe(false);
185
+ });
186
+
187
+ it("shell() produces shellCmd activity with cmd arg", () => {
188
+ const a = shell("echo hello");
189
+ expect(a.fn).toBe("shellCmd");
190
+ expect(a.args?.cmd).toBe("echo hello");
191
+ });
192
+
193
+ it("teardown() produces chantTeardown activity with longInfra profile", () => {
194
+ const a = teardown("./project");
195
+ expect(a.fn).toBe("chantTeardown");
196
+ expect(a.args?.path).toBe("./project");
197
+ expect(a.profile).toBe("longInfra");
198
+ });
199
+ });
@@ -0,0 +1,8 @@
1
+ import { createResource } from "../runtime";
2
+
3
+ /**
4
+ * The Declarable resource backing an Op definition.
5
+ * entityType: "Temporal::Op", lexicon: "temporal"
6
+ * Discovered automatically alongside infra files — no pipeline changes needed.
7
+ */
8
+ export const OpResource = createResource("Temporal::Op", "temporal", {});
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Op type definitions — the data model for a named, phased Temporal workflow.
3
+ *
4
+ * These types are intentionally free of Temporal SDK imports so they can live
5
+ * in core without pulling in @temporalio/* as a dependency.
6
+ */
7
+
8
+ export interface OpConfig {
9
+ /** Kebab-case identifier. Used as the workflow function name (camelCase) and output directory name. */
10
+ name: string;
11
+ /** Human-readable description shown in `chant run list` and deployment reports. */
12
+ overview: string;
13
+ /** Temporal task queue. Defaults to `name`. */
14
+ taskQueue?: string;
15
+ /** Temporal namespace. Defaults to chant.config.ts defaultProfile's namespace. */
16
+ namespace?: string;
17
+ /** Ordered list of execution phases. */
18
+ phases: PhaseDefinition[];
19
+ /** Other Op names that must be complete before this Op can run. */
20
+ depends?: string[];
21
+ /** Compensation phases executed on terminal failure (run in reverse order). */
22
+ onFailure?: PhaseDefinition[];
23
+ /** Search attributes to upsert at workflow start. */
24
+ searchAttributes?: Record<string, string>;
25
+ }
26
+
27
+ export interface PhaseDefinition {
28
+ /** Display name shown in progress output and Temporal UI. */
29
+ name: string;
30
+ /** Ordered steps within the phase. */
31
+ steps: StepDefinition[];
32
+ /** Run all steps concurrently via Promise.all. Default: false. */
33
+ parallel?: boolean;
34
+ }
35
+
36
+ export type StepDefinition = ActivityStep | GateStep;
37
+
38
+ export interface ActivityStep {
39
+ kind: "activity";
40
+ /** Name of the exported activity function in the pre-built activity library. */
41
+ fn: string;
42
+ /** Arguments passed to the activity function. */
43
+ args?: Record<string, unknown>;
44
+ /**
45
+ * Key from TEMPORAL_ACTIVITY_PROFILES controlling timeout + retry.
46
+ * Default: "fastIdempotent"
47
+ */
48
+ profile?: "fastIdempotent" | "longInfra" | "k8sWait" | "humanGate";
49
+ }
50
+
51
+ export interface GateStep {
52
+ kind: "gate";
53
+ /** Signal name. The generated workflow waits for this signal before continuing. */
54
+ signalName: string;
55
+ /** Temporal duration string. Default: "48h". */
56
+ timeout?: string;
57
+ /** Human-readable description of the action required to unblock this gate. */
58
+ description?: string;
59
+ }
60
+