@intentius/chant 0.1.6 → 0.1.8
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 +1 -1
- package/src/cli/commands/build.test.ts +58 -5
- package/src/cli/commands/build.ts +24 -3
- package/src/cli/handlers/graph.test.ts +91 -0
- package/src/cli/handlers/graph.ts +23 -0
- package/src/cli/handlers/run-client.ts +134 -0
- package/src/cli/handlers/run-report.ts +160 -0
- package/src/cli/handlers/run.test.ts +448 -0
- package/src/cli/handlers/run.ts +453 -0
- package/src/cli/handlers/state.test.ts +409 -0
- package/src/cli/handlers/state.ts +232 -10
- package/src/cli/main.test.ts +65 -0
- package/src/cli/main.ts +32 -18
- package/src/cli/mcp/op-tools.ts +204 -0
- package/src/cli/mcp/resource-handlers.ts +69 -50
- package/src/cli/mcp/resources/context.ts +27 -0
- package/src/cli/mcp/server.test.ts +176 -3
- package/src/cli/mcp/server.ts +7 -3
- package/src/cli/mcp/state-tools.ts +0 -51
- package/src/cli/mcp/tools/search.ts +6 -1
- package/src/cli/registry.ts +3 -0
- package/src/composite.ts +10 -5
- package/src/index.ts +1 -2
- package/src/lexicon-plugin-helpers.ts +13 -5
- package/src/lexicon.ts +57 -1
- package/src/lint/config.test.ts +21 -0
- package/src/lint/config.ts +19 -3
- package/src/op/discover.test.ts +43 -0
- package/src/op/discover.ts +89 -0
- package/src/op/index.ts +3 -1
- package/src/op/types.ts +13 -6
- package/src/state/digest.test.ts +117 -0
- package/src/state/git.test.ts +191 -0
- package/src/state/git.ts +63 -11
- package/src/state/live-diff.test.ts +184 -0
- package/src/state/live-diff.ts +215 -0
- package/src/state/snapshot.test.ts +171 -0
- package/src/state/snapshot.ts +39 -19
- package/src/state/types.ts +4 -2
- package/src/cli/handlers/spell.ts +0 -396
- package/src/spell/discovery.ts +0 -183
- package/src/spell/index.ts +0 -3
- package/src/spell/prompt.ts +0 -133
- package/src/spell/types.ts +0 -89
package/src/cli/main.test.ts
CHANGED
|
@@ -198,6 +198,7 @@ describe("resolveCommand", () => {
|
|
|
198
198
|
watch: false,
|
|
199
199
|
verbose: false,
|
|
200
200
|
help: false,
|
|
201
|
+
live: false,
|
|
201
202
|
...overrides,
|
|
202
203
|
};
|
|
203
204
|
}
|
|
@@ -254,4 +255,68 @@ describe("resolveCommand", () => {
|
|
|
254
255
|
expect(result!.def.name).toBe("dev generate");
|
|
255
256
|
expect(result!.compound).toBe(true);
|
|
256
257
|
});
|
|
258
|
+
|
|
259
|
+
test("resolves run status as compound command", () => {
|
|
260
|
+
const registry: CommandDef[] = [
|
|
261
|
+
{ name: "run list", handler: noop },
|
|
262
|
+
{ name: "run status", handler: noop },
|
|
263
|
+
{ name: "run signal", handler: noop },
|
|
264
|
+
{ name: "run cancel", handler: noop },
|
|
265
|
+
{ name: "run log", handler: noop },
|
|
266
|
+
{ name: "run", handler: noop },
|
|
267
|
+
];
|
|
268
|
+
const result = resolveCommand(makeArgs({ command: "run", path: "status" }), registry);
|
|
269
|
+
expect(result!.def.name).toBe("run status");
|
|
270
|
+
expect(result!.compound).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("resolves run <name> as simple command", () => {
|
|
274
|
+
const registry: CommandDef[] = [
|
|
275
|
+
{ name: "run list", handler: noop },
|
|
276
|
+
{ name: "run status", handler: noop },
|
|
277
|
+
{ name: "run", handler: noop },
|
|
278
|
+
];
|
|
279
|
+
const result = resolveCommand(makeArgs({ command: "run", path: "alb-deploy" }), registry);
|
|
280
|
+
expect(result!.def.name).toBe("run");
|
|
281
|
+
expect(result!.compound).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("parseArgs — run flags", () => {
|
|
286
|
+
test("parses --profile flag", () => {
|
|
287
|
+
const result = parseArgs(["run", "alb-deploy", "--profile", "local"]);
|
|
288
|
+
expect(result.command).toBe("run");
|
|
289
|
+
expect(result.path).toBe("alb-deploy");
|
|
290
|
+
expect(result.profile).toBe("local");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("parses -p shorthand for --profile", () => {
|
|
294
|
+
const result = parseArgs(["run", "alb-deploy", "-p", "cloud"]);
|
|
295
|
+
expect(result.profile).toBe("cloud");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("parses --report flag", () => {
|
|
299
|
+
const result = parseArgs(["run", "alb-deploy", "--report"]);
|
|
300
|
+
expect(result.command).toBe("run");
|
|
301
|
+
expect(result.path).toBe("alb-deploy");
|
|
302
|
+
expect(result.report).toBe(true);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("report is undefined when not provided", () => {
|
|
306
|
+
const result = parseArgs(["run", "alb-deploy"]);
|
|
307
|
+
expect(result.report).toBe(undefined);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("profile is undefined when not provided", () => {
|
|
311
|
+
const result = parseArgs(["run", "alb-deploy"]);
|
|
312
|
+
expect(result.profile).toBe(undefined);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("run signal parses op name and signal into positionals", () => {
|
|
316
|
+
const result = parseArgs(["run", "signal", "alb-deploy", "gate-dns"]);
|
|
317
|
+
expect(result.command).toBe("run");
|
|
318
|
+
expect(result.path).toBe("signal");
|
|
319
|
+
expect(result.extraPositional).toBe("alb-deploy");
|
|
320
|
+
expect(result.extraPositional2).toBe("gate-dns");
|
|
321
|
+
});
|
|
257
322
|
});
|
package/src/cli/main.ts
CHANGED
|
@@ -13,7 +13,8 @@ import { runServeLsp, runServeMcp, runServeUnknown } from "./handlers/serve";
|
|
|
13
13
|
import { runInit, runInitLexicon } from "./handlers/init";
|
|
14
14
|
import { runList, runImport, runUpdate, runDoctor } from "./handlers/misc";
|
|
15
15
|
import { runStateSnapshot, runStateShow, runStateDiff, runStateLog, runStateUnknown } from "./handlers/state";
|
|
16
|
-
import {
|
|
16
|
+
import { runGraph } from "./handlers/graph";
|
|
17
|
+
import { runOp, runOpList, runOpStatus, runOpSignal, runOpCancel, runOpLog } from "./handlers/run";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Parse command line arguments
|
|
@@ -33,6 +34,9 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
33
34
|
watch: false,
|
|
34
35
|
verbose: false,
|
|
35
36
|
help: false,
|
|
37
|
+
profile: undefined,
|
|
38
|
+
report: undefined,
|
|
39
|
+
live: false,
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
let i = 0;
|
|
@@ -57,6 +61,12 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
57
61
|
result.watch = true;
|
|
58
62
|
} else if (arg === "--verbose" || arg === "-v") {
|
|
59
63
|
result.verbose = true;
|
|
64
|
+
} else if (arg === "--profile" || arg === "-p") {
|
|
65
|
+
result.profile = args[++i];
|
|
66
|
+
} else if (arg === "--report") {
|
|
67
|
+
result.report = true;
|
|
68
|
+
} else if (arg === "--live") {
|
|
69
|
+
result.live = true;
|
|
60
70
|
} else if (!arg.startsWith("-")) {
|
|
61
71
|
if (!result.command) {
|
|
62
72
|
result.command = arg;
|
|
@@ -93,19 +103,21 @@ Commands:
|
|
|
93
103
|
list List discovered entities
|
|
94
104
|
import Import external template into TypeScript
|
|
95
105
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
Ops:
|
|
107
|
+
run <name> Start an Op workflow (spawns worker + submits to Temporal)
|
|
108
|
+
run list List all Ops with current run status
|
|
109
|
+
run status <name> Show current workflow run state
|
|
110
|
+
run signal <name> <signal> Send a named signal to unblock a gate
|
|
111
|
+
run cancel <name> Cancel the active workflow run (requires --force)
|
|
112
|
+
run log <name> Show run history for an Op
|
|
113
|
+
|
|
114
|
+
graph Show Op dependency graph
|
|
104
115
|
|
|
105
116
|
State:
|
|
106
117
|
state snapshot <env> Query API, save metadata to orphan branch
|
|
107
118
|
state show <env> Show latest state snapshot
|
|
108
119
|
state diff <env> Compare current build against last snapshot
|
|
120
|
+
--live: query cloud now and detect drift
|
|
109
121
|
state log [env] History of state snapshots
|
|
110
122
|
|
|
111
123
|
Lexicon development:
|
|
@@ -135,6 +147,8 @@ Options:
|
|
|
135
147
|
-w, --watch Watch for changes and rebuild/re-lint (build, lint)
|
|
136
148
|
-v, --verbose Show stack traces on errors
|
|
137
149
|
-h, --help Show this help message
|
|
150
|
+
-p, --profile <name> Temporal worker profile to use (run command)
|
|
151
|
+
--report Print deployment report instead of running (run command)
|
|
138
152
|
|
|
139
153
|
Examples:
|
|
140
154
|
chant build ./infra/
|
|
@@ -194,13 +208,14 @@ const registry: CommandDef[] = [
|
|
|
194
208
|
{ name: "dev onboard", handler: runDevOnboard },
|
|
195
209
|
{ name: "dev check-lexicon", handler: runDevCheckLexicon },
|
|
196
210
|
|
|
197
|
-
//
|
|
198
|
-
{ name: "
|
|
199
|
-
{ name: "
|
|
200
|
-
{ name: "
|
|
201
|
-
{ name: "
|
|
202
|
-
{ name: "
|
|
203
|
-
{ name: "
|
|
211
|
+
// Op / run subcommands
|
|
212
|
+
{ name: "run list", handler: runOpList },
|
|
213
|
+
{ name: "run status", handler: runOpStatus },
|
|
214
|
+
{ name: "run signal", handler: runOpSignal },
|
|
215
|
+
{ name: "run cancel", handler: runOpCancel },
|
|
216
|
+
{ name: "run log", handler: runOpLog },
|
|
217
|
+
{ name: "run", handler: runOp },
|
|
218
|
+
|
|
204
219
|
{ name: "graph", handler: runGraph },
|
|
205
220
|
|
|
206
221
|
// State subcommands
|
|
@@ -214,7 +229,6 @@ const registry: CommandDef[] = [
|
|
|
214
229
|
{ name: "serve mcp", requiresPlugins: true, handler: runServeMcp },
|
|
215
230
|
|
|
216
231
|
// Fallback for unknown subcommands (must come after compound entries)
|
|
217
|
-
{ name: "spell", handler: runSpellUnknown },
|
|
218
232
|
{ name: "state", handler: runStateUnknown },
|
|
219
233
|
{ name: "dev", handler: runDevUnknown },
|
|
220
234
|
{ name: "serve", handler: runServeUnknown },
|
|
@@ -250,7 +264,7 @@ async function main(): Promise<void> {
|
|
|
250
264
|
process.exit(1);
|
|
251
265
|
}
|
|
252
266
|
|
|
253
|
-
// For compound commands (e.g. "
|
|
267
|
+
// For compound commands (e.g. "run list"), args.path is the subcommand,
|
|
254
268
|
// so always use "." as the project path. For simple commands, use args.path.
|
|
255
269
|
const projectPath = match.compound ? (args.extraPositional || ".") : args.path;
|
|
256
270
|
const plugins = match.def.requiresPlugins
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { discoverOps } from "../../op/discover";
|
|
3
|
+
import { makeTemporalClient } from "../handlers/run";
|
|
4
|
+
import { resolveWorkflowId } from "../handlers/run-client";
|
|
5
|
+
import { generateReport } from "../handlers/run-report";
|
|
6
|
+
import type { ToolRegistration } from "./state-tools";
|
|
7
|
+
|
|
8
|
+
function workflowFnName(opName: string): string {
|
|
9
|
+
return opName.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()) + "Workflow";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createOpListTool(): ToolRegistration {
|
|
13
|
+
return {
|
|
14
|
+
definition: {
|
|
15
|
+
name: "op-list",
|
|
16
|
+
description: "List all Op definitions discovered from *.op.ts files with their current run status",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
profile: { type: "string", description: "Temporal profile name from chant.config.ts (optional)" },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
handler: async (params) => {
|
|
25
|
+
const { ops, errors } = await discoverOps();
|
|
26
|
+
const profile = params.profile as string | undefined;
|
|
27
|
+
|
|
28
|
+
let client: Awaited<ReturnType<typeof makeTemporalClient>>["client"] | undefined;
|
|
29
|
+
try {
|
|
30
|
+
({ client } = await makeTemporalClient(profile, resolve(".")));
|
|
31
|
+
} catch {
|
|
32
|
+
// Temporal not available — degrade gracefully
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = [];
|
|
36
|
+
for (const [name, { config }] of ops) {
|
|
37
|
+
let runStatus = "—";
|
|
38
|
+
if (client) {
|
|
39
|
+
try {
|
|
40
|
+
const desc = await client.workflow.getHandle(resolveWorkflowId(name)).describe();
|
|
41
|
+
runStatus = desc.status.name;
|
|
42
|
+
} catch {
|
|
43
|
+
// workflow not found or Temporal error
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
result.push({
|
|
47
|
+
name,
|
|
48
|
+
overview: config.overview,
|
|
49
|
+
phases: config.phases.length,
|
|
50
|
+
taskQueue: config.taskQueue ?? config.name,
|
|
51
|
+
depends: config.depends ?? [],
|
|
52
|
+
runStatus,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { ops: result, errors };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createOpRunTool(): ToolRegistration {
|
|
62
|
+
return {
|
|
63
|
+
definition: {
|
|
64
|
+
name: "op-run",
|
|
65
|
+
description:
|
|
66
|
+
"Submit an Op workflow to Temporal. The worker must already be running — start it first with `chant run <name>`.",
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
name: { type: "string", description: "Op name (e.g. alb-deploy)" },
|
|
71
|
+
profile: { type: "string", description: "Temporal profile name from chant.config.ts (optional)" },
|
|
72
|
+
},
|
|
73
|
+
required: ["name"],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
handler: async (params) => {
|
|
77
|
+
const name = params.name as string;
|
|
78
|
+
const profile = params.profile as string | undefined;
|
|
79
|
+
|
|
80
|
+
const { ops } = await discoverOps();
|
|
81
|
+
if (!ops.has(name)) {
|
|
82
|
+
const available = [...ops.keys()];
|
|
83
|
+
return `Op "${name}" not found. Available: ${available.join(", ") || "none"}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { config } = ops.get(name)!;
|
|
87
|
+
const { client, profile: resolvedProfile } = await makeTemporalClient(profile, resolve("."));
|
|
88
|
+
const workflowId = resolveWorkflowId(name);
|
|
89
|
+
const taskQueue = resolvedProfile.taskQueue ?? config.taskQueue ?? name;
|
|
90
|
+
|
|
91
|
+
await client.workflow.start(workflowFnName(name), {
|
|
92
|
+
taskQueue,
|
|
93
|
+
workflowId,
|
|
94
|
+
workflowIdConflictPolicy: "FAIL",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return { workflowId, message: `Workflow "${workflowId}" submitted to task queue "${taskQueue}"` };
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createOpStatusTool(): ToolRegistration {
|
|
103
|
+
return {
|
|
104
|
+
definition: {
|
|
105
|
+
name: "op-status",
|
|
106
|
+
description: "Return the current run state of an Op workflow",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
name: { type: "string", description: "Op name (e.g. alb-deploy)" },
|
|
111
|
+
profile: { type: "string", description: "Temporal profile name from chant.config.ts (optional)" },
|
|
112
|
+
},
|
|
113
|
+
required: ["name"],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
handler: async (params) => {
|
|
117
|
+
const name = params.name as string;
|
|
118
|
+
const profile = params.profile as string | undefined;
|
|
119
|
+
|
|
120
|
+
const { client } = await makeTemporalClient(profile, resolve("."));
|
|
121
|
+
const handle = client.workflow.getHandle(resolveWorkflowId(name));
|
|
122
|
+
const desc = await handle.describe();
|
|
123
|
+
const history = await handle.fetchHistory();
|
|
124
|
+
|
|
125
|
+
const events = history.events ?? [];
|
|
126
|
+
const activitiesCompleted = events.filter((e) => e.eventType === "ActivityTaskCompleted").length;
|
|
127
|
+
const activitiesScheduled = events.filter((e) => e.eventType === "ActivityTaskScheduled").length;
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
workflowId: desc.workflowId,
|
|
131
|
+
runId: desc.runId,
|
|
132
|
+
status: desc.status.name,
|
|
133
|
+
startTime: desc.startTime,
|
|
134
|
+
closeTime: desc.closeTime ?? null,
|
|
135
|
+
taskQueue: desc.taskQueue,
|
|
136
|
+
activitiesCompleted,
|
|
137
|
+
activitiesScheduled,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createOpSignalTool(): ToolRegistration {
|
|
144
|
+
return {
|
|
145
|
+
definition: {
|
|
146
|
+
name: "op-signal",
|
|
147
|
+
description: "Send a named signal to an Op workflow (e.g. to unblock a gate step)",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
name: { type: "string", description: "Op name (e.g. alb-deploy)" },
|
|
152
|
+
signal: { type: "string", description: "Signal name (e.g. gate-dns-delegation)" },
|
|
153
|
+
profile: { type: "string", description: "Temporal profile name from chant.config.ts (optional)" },
|
|
154
|
+
},
|
|
155
|
+
required: ["name", "signal"],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
handler: async (params) => {
|
|
159
|
+
const name = params.name as string;
|
|
160
|
+
const signal = params.signal as string;
|
|
161
|
+
const profile = params.profile as string | undefined;
|
|
162
|
+
|
|
163
|
+
const { client } = await makeTemporalClient(profile, resolve("."));
|
|
164
|
+
const handle = client.workflow.getHandle(resolveWorkflowId(name));
|
|
165
|
+
await handle.signal(signal);
|
|
166
|
+
|
|
167
|
+
return `Signal "${signal}" sent to Op "${name}"`;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function createOpReportTool(): ToolRegistration {
|
|
173
|
+
return {
|
|
174
|
+
definition: {
|
|
175
|
+
name: "op-report",
|
|
176
|
+
description: "Return a markdown deployment report for the latest run of an Op",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: {
|
|
180
|
+
name: { type: "string", description: "Op name (e.g. alb-deploy)" },
|
|
181
|
+
profile: { type: "string", description: "Temporal profile name from chant.config.ts (optional)" },
|
|
182
|
+
},
|
|
183
|
+
required: ["name"],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
handler: async (params) => {
|
|
187
|
+
const name = params.name as string;
|
|
188
|
+
const profile = params.profile as string | undefined;
|
|
189
|
+
|
|
190
|
+
const { ops } = await discoverOps();
|
|
191
|
+
if (!ops.has(name)) {
|
|
192
|
+
return `Op "${name}" not found`;
|
|
193
|
+
}
|
|
194
|
+
const { config } = ops.get(name)!;
|
|
195
|
+
|
|
196
|
+
const { client } = await makeTemporalClient(profile, resolve("."));
|
|
197
|
+
const handle = client.workflow.getHandle(resolveWorkflowId(name));
|
|
198
|
+
const desc = await handle.describe();
|
|
199
|
+
const history = await handle.fetchHistory();
|
|
200
|
+
|
|
201
|
+
return generateReport(name, config, desc, history);
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import type { ResourceDefinition } from "./types";
|
|
2
3
|
import { getContext } from "./resources/context";
|
|
3
4
|
import { readSnapshot, readEnvironmentSnapshots } from "../../state/git";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { discoverOps } from "../../op/discover";
|
|
6
|
+
import { makeTemporalClient } from "../handlers/run";
|
|
7
|
+
import { resolveWorkflowId } from "../handlers/run-client";
|
|
7
8
|
|
|
8
9
|
type PluginResourceEntry = { definition: ResourceDefinition; handler: () => Promise<string> };
|
|
9
10
|
|
|
@@ -24,22 +25,22 @@ export const coreResourceDefinitions: ResourceDefinition[] = [
|
|
|
24
25
|
mimeType: "application/json",
|
|
25
26
|
},
|
|
26
27
|
{
|
|
27
|
-
uri: "chant://
|
|
28
|
-
name: "
|
|
29
|
-
description: "
|
|
28
|
+
uri: "chant://ops",
|
|
29
|
+
name: "Ops",
|
|
30
|
+
description: "All Op definitions discovered from *.op.ts files",
|
|
30
31
|
mimeType: "application/json",
|
|
31
32
|
},
|
|
32
33
|
{
|
|
33
|
-
uri: "chant://
|
|
34
|
-
name: "
|
|
35
|
-
description: "
|
|
34
|
+
uri: "chant://ops/{name}/runs",
|
|
35
|
+
name: "Op run history",
|
|
36
|
+
description: "Workflow run history for a named Op",
|
|
36
37
|
mimeType: "application/json",
|
|
37
38
|
},
|
|
38
39
|
{
|
|
39
|
-
uri: "chant://
|
|
40
|
-
name: "
|
|
41
|
-
description: "
|
|
42
|
-
mimeType: "
|
|
40
|
+
uri: "chant://ops/{name}/runs/latest",
|
|
41
|
+
name: "Op latest run",
|
|
42
|
+
description: "Latest run state for a named Op",
|
|
43
|
+
mimeType: "application/json",
|
|
43
44
|
},
|
|
44
45
|
{
|
|
45
46
|
uri: "chant://state/{environment}",
|
|
@@ -84,6 +85,10 @@ export function collectExamples(
|
|
|
84
85
|
return examples;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
function opWorkflowFnName(opName: string): string {
|
|
89
|
+
return opName.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()) + "Workflow";
|
|
90
|
+
}
|
|
91
|
+
|
|
87
92
|
/**
|
|
88
93
|
* Handle resources/read request — checks plugin resources after core
|
|
89
94
|
*/
|
|
@@ -117,51 +122,65 @@ export async function handleResourcesRead(
|
|
|
117
122
|
};
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
//
|
|
121
|
-
if (uri === "chant://
|
|
122
|
-
const {
|
|
123
|
-
const list = Array.from(
|
|
124
|
-
name,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
// Op resources
|
|
126
|
+
if (uri === "chant://ops") {
|
|
127
|
+
const { ops } = await discoverOps();
|
|
128
|
+
const list = Array.from(ops.values()).map(({ config }) => ({
|
|
129
|
+
name: config.name,
|
|
130
|
+
overview: config.overview,
|
|
131
|
+
phases: config.phases.length,
|
|
132
|
+
taskQueue: config.taskQueue ?? config.name,
|
|
133
|
+
depends: config.depends ?? [],
|
|
129
134
|
}));
|
|
130
135
|
return {
|
|
131
136
|
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(list, null, 2) }],
|
|
132
137
|
};
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
if (uri.startsWith("chant://
|
|
136
|
-
const name = uri.replace("chant://
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
if (uri.startsWith("chant://ops/") && uri.endsWith("/runs/latest")) {
|
|
141
|
+
const name = uri.replace("chant://ops/", "").replace("/runs/latest", "");
|
|
142
|
+
try {
|
|
143
|
+
const { client } = await makeTemporalClient(undefined, resolve("."));
|
|
144
|
+
const handle = client.workflow.getHandle(resolveWorkflowId(name));
|
|
145
|
+
const desc = await handle.describe();
|
|
146
|
+
const history = await handle.fetchHistory();
|
|
147
|
+
const events = history.events ?? [];
|
|
148
|
+
const result = {
|
|
149
|
+
workflowId: desc.workflowId,
|
|
150
|
+
runId: desc.runId,
|
|
151
|
+
status: desc.status.name,
|
|
152
|
+
startTime: desc.startTime,
|
|
153
|
+
closeTime: desc.closeTime ?? null,
|
|
154
|
+
taskQueue: desc.taskQueue,
|
|
155
|
+
activitiesCompleted: events.filter((e) => e.eventType === "ActivityTaskCompleted").length,
|
|
156
|
+
activitiesScheduled: events.filter((e) => e.eventType === "ActivityTaskScheduled").length,
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(result, null, 2) }],
|
|
160
|
+
};
|
|
161
|
+
} catch (err) {
|
|
162
|
+
return {
|
|
163
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }) }],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
147
166
|
}
|
|
148
167
|
|
|
149
|
-
if (uri.startsWith("chant://
|
|
150
|
-
const name = uri.replace("chant://
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
text: JSON.stringify(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
}
|
|
168
|
+
if (uri.startsWith("chant://ops/") && uri.endsWith("/runs")) {
|
|
169
|
+
const name = uri.replace("chant://ops/", "").replace("/runs", "");
|
|
170
|
+
try {
|
|
171
|
+
const { client } = await makeTemporalClient(undefined, resolve("."));
|
|
172
|
+
const runs: unknown[] = [];
|
|
173
|
+
for await (const run of client.workflow.list({ query: `WorkflowType = "${opWorkflowFnName(name)}"` })) {
|
|
174
|
+
runs.push({ runId: run.runId, status: run.status.name, startTime: run.startTime, closeTime: run.closeTime ?? null });
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(runs, null, 2) }],
|
|
178
|
+
};
|
|
179
|
+
} catch (err) {
|
|
180
|
+
return {
|
|
181
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }) }],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
165
184
|
}
|
|
166
185
|
|
|
167
186
|
// State resources: chant://state/{environment} and chant://state/{environment}/{lexicon}
|
|
@@ -49,6 +49,33 @@ chant import template.json # Convert external template
|
|
|
49
49
|
chant import template.json -o ./src/ # Custom output dir
|
|
50
50
|
\`\`\`
|
|
51
51
|
|
|
52
|
+
## Ops (Temporal Workflows)
|
|
53
|
+
|
|
54
|
+
Ops are durable workflow definitions backed by Temporal. Each \`*.op.ts\` file declares an Op with named phases and activity steps.
|
|
55
|
+
|
|
56
|
+
### Op MCP Tools
|
|
57
|
+
|
|
58
|
+
| Tool | Description |
|
|
59
|
+
|---|---|
|
|
60
|
+
| \`op-list\` | List all discovered Ops with current run status. Optional \`profile\` param. |
|
|
61
|
+
| \`op-run\` | Submit an Op workflow. Requires \`name\`. Worker must already be running via \`chant run <name>\`. |
|
|
62
|
+
| \`op-status\` | Get current run state (status, activity counts, times). Requires \`name\`. |
|
|
63
|
+
| \`op-signal\` | Send a signal to unblock a gate step. Requires \`name\` and \`signal\`. |
|
|
64
|
+
| \`op-report\` | Return a markdown deployment report for the latest run. Requires \`name\`. |
|
|
65
|
+
|
|
66
|
+
All Op tools accept an optional \`profile\` parameter matching a profile name in \`chant.config.ts\` \`temporal.profiles\`.
|
|
67
|
+
|
|
68
|
+
### Op MCP Resources
|
|
69
|
+
|
|
70
|
+
| URI | Description |
|
|
71
|
+
|---|---|
|
|
72
|
+
| \`chant://ops\` | JSON array of all Op definitions (name, overview, phases, taskQueue, depends) |
|
|
73
|
+
| \`chant://ops/{name}/runs\` | Workflow run history for a named Op |
|
|
74
|
+
| \`chant://ops/{name}/runs/latest\` | Latest run state for a named Op |
|
|
75
|
+
|
|
76
|
+
### Workflow IDs
|
|
77
|
+
Ops use deterministic workflow IDs: \`chant-op-<opName>\` (e.g. \`chant-op-alb-deploy\`).
|
|
78
|
+
|
|
52
79
|
## Best Practices
|
|
53
80
|
|
|
54
81
|
1. **Flat Declarations**: Keep declarations at module level, avoid deep nesting
|