@intentius/chant 0.1.13 → 0.1.15

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 (52) hide show
  1. package/package.json +1 -1
  2. package/src/build.ts +18 -2
  3. package/src/cli/commands/build.ts +9 -1
  4. package/src/cli/commands/import-live.test.ts +126 -0
  5. package/src/cli/commands/import.ts +152 -2
  6. package/src/cli/commands/migrate.ts +2 -2
  7. package/src/cli/handlers/{state.test.ts → lifecycle.test.ts} +37 -37
  8. package/src/cli/handlers/{state.ts → lifecycle.ts} +148 -36
  9. package/src/cli/handlers/misc.ts +31 -2
  10. package/src/cli/handlers/run.test.ts +98 -0
  11. package/src/cli/handlers/run.ts +123 -0
  12. package/src/cli/main.test.ts +14 -0
  13. package/src/cli/main.ts +38 -12
  14. package/src/cli/mcp/{state-tools.ts → lifecycle-tools.ts} +9 -9
  15. package/src/cli/mcp/op-tools.ts +2 -2
  16. package/src/cli/mcp/resource-handlers.ts +1 -1
  17. package/src/cli/mcp/server.test.ts +2 -2
  18. package/src/cli/mcp/server.ts +1 -1
  19. package/src/cli/registry.ts +21 -2
  20. package/src/codegen/fetch.test.ts +103 -2
  21. package/src/codegen/fetch.ts +62 -10
  22. package/src/config.ts +31 -0
  23. package/src/detectLexicon.test.ts +2 -2
  24. package/src/index.ts +2 -2
  25. package/src/lexicon-export.test.ts +92 -0
  26. package/src/lexicon.ts +88 -9
  27. package/src/lifecycle/change-set.test.ts +151 -0
  28. package/src/lifecycle/change-set.ts +172 -0
  29. package/src/{state → lifecycle}/git.test.ts +15 -15
  30. package/src/{state → lifecycle}/git.ts +14 -14
  31. package/src/{state → lifecycle}/index.ts +2 -0
  32. package/src/{state → lifecycle}/snapshot.test.ts +5 -5
  33. package/src/{state → lifecycle}/snapshot.ts +9 -9
  34. package/src/{state → lifecycle}/types.ts +1 -1
  35. package/src/op/activity-registry.test.ts +59 -0
  36. package/src/op/activity-registry.ts +91 -0
  37. package/src/op/builders.ts +3 -3
  38. package/src/op/index.ts +6 -1
  39. package/src/op/local-executor.test.ts +247 -0
  40. package/src/op/local-executor.ts +300 -0
  41. package/src/op/local-output.test.ts +54 -0
  42. package/src/op/local-output.ts +63 -0
  43. package/src/op/op.test.ts +4 -4
  44. package/src/op/types.ts +1 -1
  45. package/src/ownership.test.ts +109 -0
  46. package/src/ownership.ts +142 -0
  47. package/src/serializer.ts +19 -1
  48. package/src/toml-parse.ts +3 -3
  49. /package/src/{state → lifecycle}/digest.test.ts +0 -0
  50. /package/src/{state → lifecycle}/digest.ts +0 -0
  51. /package/src/{state → lifecycle}/live-diff.test.ts +0 -0
  52. /package/src/{state → lifecycle}/live-diff.ts +0 -0
@@ -43,6 +43,9 @@ function makeArgs(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
43
43
  return {
44
44
  command: "run", path: ".",
45
45
  format: "", fix: false, watch: false, verbose: false, help: false, live: false,
46
+ // These suites exercise the Temporal path; local mode is the CLI default,
47
+ // so opt in explicitly here. Local-mode behavior is covered separately below.
48
+ temporal: true,
46
49
  ...overrides,
47
50
  };
48
51
  }
@@ -446,3 +449,98 @@ describe("runOp", () => {
446
449
  }
447
450
  });
448
451
  });
452
+
453
+ // ── local mode dispatcher + guards ──────────────────────────────────────────
454
+
455
+ function localOp(name: string, steps: unknown[]) {
456
+ return [name, { config: { name, overview: `${name} overview`, phases: [{ name: "Phase", steps }] } }] as const;
457
+ }
458
+
459
+ describe("runOp dispatcher", () => {
460
+ beforeEach(() => {
461
+ discoverOpsMock.mockReset();
462
+ loadTemporalClientMock.mockReset();
463
+ loadChantConfigMock.mockReset();
464
+ resolveProfileMock.mockReset();
465
+ existsSyncMock.mockReset();
466
+ spawnChildMock.mockReset();
467
+ });
468
+
469
+ test("no flag → local executor (no Temporal client or worker spawned)", async () => {
470
+ discoverOpsMock.mockResolvedValue({
471
+ ops: new Map([localOp("hello", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
472
+ errors: [],
473
+ });
474
+ const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
475
+ const exit = await runOp({ args: makeArgs({ path: "hello", temporal: false }), plugins: [], serializers: [] });
476
+ expect(exit).toBe(0);
477
+ expect(loadTemporalClientMock).not.toHaveBeenCalled();
478
+ expect(spawnChildMock).not.toHaveBeenCalled();
479
+ stderrWrite.mockRestore();
480
+ });
481
+
482
+ test("--temporal → Temporal path (missing worker.ts → exit 1)", async () => {
483
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
484
+ loadChantConfigMock.mockResolvedValue({ config: {} });
485
+ resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
486
+ existsSyncMock.mockReturnValue(false);
487
+ const stderr = makeStderrSpy();
488
+ const exit = await runOp({ args: makeArgs({ path: "hello", temporal: true }), plugins: [], serializers: [] });
489
+ expect(exit).toBe(1);
490
+ expect(stderr.join("\n")).toContain("worker.ts not found");
491
+ });
492
+
493
+ test("gate in local mode → fast-fail before execution, suggests --temporal", async () => {
494
+ discoverOpsMock.mockResolvedValue({
495
+ ops: new Map([localOp("gated", [{ kind: "gate", signalName: "approve-prod" }])]),
496
+ errors: [],
497
+ });
498
+ const stderr = makeStderrSpy();
499
+ const exit = await runOp({ args: makeArgs({ path: "gated", temporal: false }), plugins: [], serializers: [] });
500
+ expect(exit).toBe(1);
501
+ expect(stderr.join("\n")).toContain("--temporal");
502
+ expect(loadTemporalClientMock).not.toHaveBeenCalled();
503
+ });
504
+
505
+ test("--local and --temporal together → exit 1 before any work", async () => {
506
+ const stderr = makeStderrSpy();
507
+ const exit = await runOp({ args: makeArgs({ path: "hello", local: true, temporal: true }), plugins: [], serializers: [] });
508
+ expect(exit).toBe(1);
509
+ expect(stderr.join("\n")).toContain("mutually exclusive");
510
+ expect(discoverOpsMock).not.toHaveBeenCalled();
511
+ expect(loadTemporalClientMock).not.toHaveBeenCalled();
512
+ });
513
+
514
+ test("--json → structured result on stdout", async () => {
515
+ discoverOpsMock.mockResolvedValue({
516
+ ops: new Map([localOp("hello", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
517
+ errors: [],
518
+ });
519
+ const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
520
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
521
+ const exit = await runOp({ args: makeArgs({ path: "hello", temporal: false, json: true }), plugins: [], serializers: [] });
522
+ expect(exit).toBe(0);
523
+ const printed = stdoutWrite.mock.calls.map((c) => String(c[0])).join("");
524
+ const parsed = JSON.parse(printed.trim());
525
+ expect(parsed.op).toBe("hello");
526
+ expect(parsed.ok).toBe(true);
527
+ vi.restoreAllMocks();
528
+ });
529
+ });
530
+
531
+ describe("Temporal-only subcommand guards", () => {
532
+ const cases: Array<[string, (ctx: { args: ParsedArgs; plugins: never[]; serializers: never[] }) => Promise<number>]> = [
533
+ ["list", runOpList],
534
+ ["status", runOpStatus],
535
+ ["log", runOpLog],
536
+ ["signal", runOpSignal],
537
+ ["cancel", runOpCancel],
538
+ ];
539
+
540
+ test.each(cases)("run %s without --temporal → exit 1 + actionable message", async (_name, handler) => {
541
+ const stderr = makeStderrSpy();
542
+ const exit = await handler({ args: makeArgs({ temporal: false, extraPositional: "x", extraPositional2: "y" }), plugins: [], serializers: [] });
543
+ expect(exit).toBe(1);
544
+ expect(stderr.join("\n")).toContain("not available in local mode");
545
+ });
546
+ });
@@ -4,6 +4,9 @@ import { createConnection } from "node:net";
4
4
  import { spawn as spawnChild, type ChildProcess } from "node:child_process";
5
5
  import { loadChantConfig } from "../../config";
6
6
  import { discoverOps } from "../../op/discover";
7
+ import { loadActivities, loadProfiles } from "../../op/activity-registry";
8
+ import { runOpLocally, findGate, LocalGateUnsupportedError, OpRunFailure } from "../../op/local-executor";
9
+ import { renderHuman, renderJson } from "../../op/local-output";
7
10
  import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
8
11
  import type { CommandContext } from "../registry";
9
12
  import {
@@ -25,6 +28,21 @@ function workflowFnName(opName: string): string {
25
28
  return kebabToCamel(opName) + "Workflow";
26
29
  }
27
30
 
31
+ /**
32
+ * Guard for Temporal-only commands (run list/status/log/signal/cancel, --report).
33
+ * These query durable run state that only exists under Temporal. Returns true
34
+ * when `--temporal` was passed; otherwise prints an actionable message and the
35
+ * caller should return non-zero.
36
+ */
37
+ function requireTemporalMode(ctx: CommandContext, what: string): boolean {
38
+ if (ctx.args.temporal) return true;
39
+ console.error(formatError({
40
+ message: `\`${what}\` is not available in local mode`,
41
+ hint: "pass --temporal or configure a profile",
42
+ }));
43
+ return false;
44
+ }
45
+
28
46
  export async function makeTemporalClient(profileName: string | undefined, projectPath: string) {
29
47
  const { config } = await loadChantConfig(projectPath);
30
48
  const profile = resolveProfile(config as Record<string, unknown>, profileName);
@@ -37,6 +55,7 @@ export async function makeTemporalClient(profileName: string | undefined, projec
37
55
  // ── chant run list ──────────────────────────────────���─────────────────────────
38
56
 
39
57
  export async function runOpList(ctx: CommandContext): Promise<number> {
58
+ if (!requireTemporalMode(ctx, "chant run list")) return 1;
40
59
  const { ops, errors } = await discoverOps();
41
60
 
42
61
  for (const err of errors) {
@@ -102,6 +121,7 @@ export async function runOpList(ctx: CommandContext): Promise<number> {
102
121
  // ── chant run status <name> ───────────────────────────────────────────────────
103
122
 
104
123
  export async function runOpStatus(ctx: CommandContext): Promise<number> {
124
+ if (!requireTemporalMode(ctx, "chant run status")) return 1;
105
125
  const name = ctx.args.extraPositional;
106
126
  if (!name) {
107
127
  console.error(formatError({ message: "Op name is required: chant run status <name>" }));
@@ -141,6 +161,7 @@ export async function runOpStatus(ctx: CommandContext): Promise<number> {
141
161
  // ── chant run log <name> ──────────────────────────────────────────────────────
142
162
 
143
163
  export async function runOpLog(ctx: CommandContext): Promise<number> {
164
+ if (!requireTemporalMode(ctx, "chant run log")) return 1;
144
165
  const name = ctx.args.extraPositional;
145
166
  if (!name) {
146
167
  console.error(formatError({ message: "Op name is required: chant run log <name>" }));
@@ -186,6 +207,7 @@ export async function runOpLog(ctx: CommandContext): Promise<number> {
186
207
  // ── chant run signal <name> <signal> ─────────────────────────────────────────
187
208
 
188
209
  export async function runOpSignal(ctx: CommandContext): Promise<number> {
210
+ if (!requireTemporalMode(ctx, "chant run signal")) return 1;
189
211
  const name = ctx.args.extraPositional;
190
212
  const signalName = ctx.args.extraPositional2;
191
213
 
@@ -212,6 +234,7 @@ export async function runOpSignal(ctx: CommandContext): Promise<number> {
212
234
  // ── chant run cancel <name> ───────────────────────────────────────────────────
213
235
 
214
236
  export async function runOpCancel(ctx: CommandContext): Promise<number> {
237
+ if (!requireTemporalMode(ctx, "chant run cancel")) return 1;
215
238
  const name = ctx.args.extraPositional;
216
239
  if (!name) {
217
240
  console.error(formatError({ message: "Op name is required: chant run cancel <name>" }));
@@ -274,7 +297,107 @@ function renderProgress(opName: string, history: WorkflowHistoryRaw): void {
274
297
  );
275
298
  }
276
299
 
300
+ /**
301
+ * `chant run <name>` dispatcher.
302
+ *
303
+ * Local mode is the default — it runs the Op in-process with no Temporal
304
+ * server. `--temporal` opts into a cluster (gates, schedules, durable resume).
305
+ * `--report` reads a past durable run and is therefore Temporal-only.
306
+ */
277
307
  export async function runOp(ctx: CommandContext): Promise<number> {
308
+ if (ctx.args.local && ctx.args.temporal) {
309
+ console.error(formatError({
310
+ message: "--local and --temporal are mutually exclusive",
311
+ hint: "omit both for local mode (the default), or pass exactly one",
312
+ }));
313
+ return 1;
314
+ }
315
+ if (ctx.args.report) {
316
+ if (!requireTemporalMode(ctx, "chant run --report")) return 1;
317
+ return runOpTemporal(ctx);
318
+ }
319
+ return ctx.args.temporal ? runOpTemporal(ctx) : runOpLocal(ctx);
320
+ }
321
+
322
+ /**
323
+ * Run an Op in-process via the local executor — no Temporal worker, server, or
324
+ * built `worker.ts`. Reads the Op config straight from discovery and resolves
325
+ * activities from the Temporal lexicon package.
326
+ */
327
+ export async function runOpLocal(ctx: CommandContext): Promise<number> {
328
+ const opName = ctx.args.path;
329
+ if (!opName || opName === ".") {
330
+ console.error(formatError({
331
+ message: "Op name is required: chant run <name>",
332
+ hint: "Run `chant run list --temporal` to see available Ops",
333
+ }));
334
+ return 1;
335
+ }
336
+
337
+ const { ops, errors } = await discoverOps();
338
+ for (const err of errors) console.error(formatWarning({ message: err }));
339
+
340
+ const discovered = ops.get(opName);
341
+ if (!discovered) {
342
+ const names = [...ops.keys()];
343
+ console.error(formatError({
344
+ message: `Op "${opName}" not found`,
345
+ hint: names.length > 0
346
+ ? `Available: ${names.join(", ")}`
347
+ : "No *.op.ts files found — create one",
348
+ }));
349
+ return 1;
350
+ }
351
+
352
+ const { config } = discovered;
353
+
354
+ // Pre-flight: gates/schedules need a durable runtime — fail before any step.
355
+ const gate = findGate(config);
356
+ if (gate) {
357
+ console.error(formatError({
358
+ message: new LocalGateUnsupportedError(gate.signalName).message,
359
+ }));
360
+ return 1;
361
+ }
362
+
363
+ let activities, profiles;
364
+ try {
365
+ [activities, profiles] = await Promise.all([loadActivities(), loadProfiles()]);
366
+ } catch (err) {
367
+ console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
368
+ return 1;
369
+ }
370
+
371
+ // Ctrl-C aborts in-flight activities (kills their child processes) instead of
372
+ // orphaning them. The handler is removed in `finally` so it never leaks.
373
+ const controller = new AbortController();
374
+ const onSigint = () => {
375
+ console.error(formatWarning({ message: "interrupted — stopping Op" }));
376
+ controller.abort();
377
+ };
378
+ process.once("SIGINT", onSigint);
379
+
380
+ try {
381
+ const result = await runOpLocally(config, activities, profiles, controller.signal);
382
+ if (ctx.args.json) renderJson(result); else renderHuman(result);
383
+ return 0;
384
+ } catch (err) {
385
+ if (err instanceof OpRunFailure) {
386
+ if (ctx.args.json) renderJson(err.result); else renderHuman(err.result);
387
+ return 1;
388
+ }
389
+ if (err instanceof LocalGateUnsupportedError) {
390
+ console.error(formatError({ message: err.message }));
391
+ return 1;
392
+ }
393
+ console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
394
+ return 1;
395
+ } finally {
396
+ process.removeListener("SIGINT", onSigint);
397
+ }
398
+ }
399
+
400
+ async function runOpTemporal(ctx: CommandContext): Promise<number> {
278
401
  const opName = ctx.args.path;
279
402
 
280
403
  if (!opName || opName === ".") {
@@ -187,6 +187,8 @@ describe("resolveCommand", () => {
187
187
  { name: "init", handler: noop },
188
188
  { name: "init lexicon", handler: noop },
189
189
  { name: "dev", handler: noop },
190
+ { name: "lifecycle plan", handler: noop },
191
+ { name: "lifecycle", handler: noop },
190
192
  ];
191
193
 
192
194
  function makeArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
@@ -256,6 +258,18 @@ describe("resolveCommand", () => {
256
258
  expect(result!.compound).toBe(true);
257
259
  });
258
260
 
261
+ test("`lc` is an alias for `lifecycle` (compound)", () => {
262
+ const result = resolveCommand(makeArgs({ command: "lc", path: "plan" }), testRegistry);
263
+ expect(result!.def.name).toBe("lifecycle plan");
264
+ expect(result!.compound).toBe(true);
265
+ });
266
+
267
+ test("`lc` is an alias for `lifecycle` (simple)", () => {
268
+ const result = resolveCommand(makeArgs({ command: "lc" }), testRegistry);
269
+ expect(result!.def.name).toBe("lifecycle");
270
+ expect(result!.compound).toBe(false);
271
+ });
272
+
259
273
  test("resolves run status as compound command", () => {
260
274
  const registry: CommandDef[] = [
261
275
  { name: "run list", handler: noop },
package/src/cli/main.ts CHANGED
@@ -13,7 +13,7 @@ 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 { runMigrate } from "./handlers/migrate";
16
- import { runStateSnapshot, runStateShow, runStateDiff, runStateLog, runStateUnknown } from "./handlers/state";
16
+ import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecyclePlan, runLifecycleLog, runLifecycleUnknown } from "./handlers/lifecycle";
17
17
  import { runGraph } from "./handlers/graph";
18
18
  import { runOp, runOpList, runOpStatus, runOpSignal, runOpCancel, runOpLog } from "./handlers/run";
19
19
 
@@ -37,6 +37,9 @@ export function parseArgs(args: string[]): ParsedArgs {
37
37
  help: false,
38
38
  profile: undefined,
39
39
  report: undefined,
40
+ local: undefined,
41
+ temporal: undefined,
42
+ json: undefined,
40
43
  live: false,
41
44
  migrateFrom: undefined,
42
45
  migrateTo: undefined,
@@ -85,7 +88,17 @@ export function parseArgs(args: string[]): ParsedArgs {
85
88
  } else if (arg === "--live") {
86
89
  result.live = true;
87
90
  } else if (arg === "--from") {
91
+ // Shared by `migrate --from <lexicon>` and `import --from <env>`; the
92
+ // two commands never run together, so one field carries both.
88
93
  result.migrateFrom = args[++i];
94
+ } else if (arg === "--type") {
95
+ result.selectType = args[++i];
96
+ } else if (arg === "--name") {
97
+ result.selectName = args[++i];
98
+ } else if (arg === "--owned") {
99
+ result.owned = true;
100
+ } else if (arg === "--verbatim") {
101
+ result.verbatim = true;
89
102
  } else if (arg === "--to") {
90
103
  result.migrateTo = args[++i];
91
104
  } else if (arg === "--emit") {
@@ -98,6 +111,12 @@ export function parseArgs(args: string[]): ParsedArgs {
98
111
  result.useComposites = true;
99
112
  } else if (arg === "--skill") {
100
113
  result.skill = args[++i];
114
+ } else if (arg === "--local") {
115
+ result.local = true;
116
+ } else if (arg === "--temporal") {
117
+ result.temporal = true;
118
+ } else if (arg === "--json") {
119
+ result.json = true;
101
120
  } else if (!arg.startsWith("-")) {
102
121
  if (!result.command) {
103
122
  result.command = arg;
@@ -146,12 +165,14 @@ Ops:
146
165
 
147
166
  graph Show Op dependency graph
148
167
 
149
- State:
150
- state snapshot <env> Query API, save metadata to orphan branch
151
- state show <env> Show latest state snapshot
152
- state diff <env> Compare current build against last snapshot
153
- --live: query cloud now and detect drift
154
- state log [env] History of state snapshots
168
+ Lifecycle (alias: lc):
169
+ lifecycle snapshot <env> Query API, save metadata to orphan branch
170
+ lifecycle show <env> Show latest lifecycle snapshot
171
+ lifecycle diff <env> Compare current build against last snapshot
172
+ --live: query cloud now and detect drift
173
+ lifecycle plan <env> Typed change set (create/update/delete/adopt) vs live
174
+ --json: emit the ChangeSet as JSON
175
+ lifecycle log [env] History of lifecycle snapshots
155
176
 
156
177
  Lexicon development:
157
178
  dev generate Generate lexicon artifacts (+ validate + coverage)
@@ -182,6 +203,9 @@ Options:
182
203
  -v, --verbose Show stack traces on errors
183
204
  -h, --help Show this help message
184
205
  -p, --profile <name> Temporal worker profile to use (run command)
206
+ --local Run an Op with the local in-process executor (default)
207
+ --temporal Run an Op via a Temporal cluster (gates, schedules, durable resume)
208
+ --json Emit the structured run result as JSON (run command)
185
209
  --report Print deployment report instead of running (run command)
186
210
  OR with a path arg: SARIF report destination (migrate)
187
211
  --from <name> Source lexicon for migrate (default: github)
@@ -197,6 +221,7 @@ Examples:
197
221
  chant build ./infra/ --format yaml
198
222
  chant build ./infra/ --watch
199
223
  chant import template.json --output ./infra/
224
+ chant import --from prod --name my-bucket --output src/
200
225
  chant lint ./infra/
201
226
  chant lint ./infra/ --format sarif
202
227
  chant lint ./infra/ --watch
@@ -261,17 +286,18 @@ const registry: CommandDef[] = [
261
286
  { name: "graph", handler: runGraph },
262
287
 
263
288
  // State subcommands
264
- { name: "state snapshot", requiresPlugins: true, handler: runStateSnapshot },
265
- { name: "state show", handler: runStateShow },
266
- { name: "state diff", requiresPlugins: true, handler: runStateDiff },
267
- { name: "state log", handler: runStateLog },
289
+ { name: "lifecycle snapshot", requiresPlugins: true, handler: runLifecycleSnapshot },
290
+ { name: "lifecycle show", handler: runLifecycleShow },
291
+ { name: "lifecycle diff", requiresPlugins: true, handler: runLifecycleDiff },
292
+ { name: "lifecycle plan", requiresPlugins: true, handler: runLifecyclePlan },
293
+ { name: "lifecycle log", handler: runLifecycleLog },
268
294
 
269
295
  // Serve subcommands
270
296
  { name: "serve lsp", requiresPlugins: true, handler: runServeLsp },
271
297
  { name: "serve mcp", requiresPlugins: true, handler: runServeMcp },
272
298
 
273
299
  // Fallback for unknown subcommands (must come after compound entries)
274
- { name: "state", handler: runStateUnknown },
300
+ { name: "lifecycle", handler: runLifecycleUnknown },
275
301
  { name: "dev", handler: runDevUnknown },
276
302
  { name: "serve", handler: runServeUnknown },
277
303
  ];
@@ -1,23 +1,23 @@
1
1
  import { resolve } from "node:path";
2
2
  import type { LexiconPlugin } from "../../lexicon";
3
3
  import type { ToolDefinition, ToolHandler } from "./types";
4
- import { readSnapshot } from "../../state/git";
4
+ import { readSnapshot } from "../../lifecycle/git";
5
5
  import { build } from "../../build";
6
- import { computeBuildDigest, diffDigests } from "../../state/digest";
7
- import { takeSnapshot } from "../../state/snapshot";
8
- import type { StateSnapshot } from "../../state/types";
6
+ import { computeBuildDigest, diffDigests } from "../../lifecycle/digest";
7
+ import { takeSnapshot } from "../../lifecycle/snapshot";
8
+ import type { LifecycleSnapshot } from "../../lifecycle/types";
9
9
  export interface ToolRegistration {
10
10
  definition: ToolDefinition;
11
11
  handler: ToolHandler;
12
12
  }
13
13
 
14
14
  /**
15
- * Create state-snapshot tool definition and handler
15
+ * Create lifecycle-snapshot tool definition and handler
16
16
  */
17
17
  export function createSnapshotTool(plugins: LexiconPlugin[]): ToolRegistration {
18
18
  return {
19
19
  definition: {
20
- name: "state-snapshot",
20
+ name: "lifecycle-snapshot",
21
21
  description: "Capture deployed state for an environment",
22
22
  inputSchema: {
23
23
  type: "object",
@@ -46,12 +46,12 @@ export function createSnapshotTool(plugins: LexiconPlugin[]): ToolRegistration {
46
46
  }
47
47
 
48
48
  /**
49
- * Create state-diff tool definition and handler
49
+ * Create lifecycle-diff tool definition and handler
50
50
  */
51
51
  export function createDiffTool(plugins: LexiconPlugin[]): ToolRegistration {
52
52
  return {
53
53
  definition: {
54
- name: "state-diff",
54
+ name: "lifecycle-diff",
55
55
  description: "Compare current build declarations against last snapshot's digest",
56
56
  inputSchema: {
57
57
  type: "object",
@@ -75,7 +75,7 @@ export function createDiffTool(plugins: LexiconPlugin[]): ToolRegistration {
75
75
  const content = await readSnapshot(env, lex);
76
76
  let previousDigest = undefined;
77
77
  if (content) {
78
- const snapshot: StateSnapshot = JSON.parse(content);
78
+ const snapshot: LifecycleSnapshot = JSON.parse(content);
79
79
  previousDigest = snapshot.digest;
80
80
  }
81
81
  results[lex] = diffDigests(currentDigest, previousDigest);
@@ -3,7 +3,7 @@ import { discoverOps } from "../../op/discover";
3
3
  import { makeTemporalClient } from "../handlers/run";
4
4
  import { resolveWorkflowId } from "../handlers/run-client";
5
5
  import { generateReport } from "../handlers/run-report";
6
- import type { ToolRegistration } from "./state-tools";
6
+ import type { ToolRegistration } from "./lifecycle-tools";
7
7
 
8
8
  function workflowFnName(opName: string): string {
9
9
  return opName.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()) + "Workflow";
@@ -63,7 +63,7 @@ export function createOpRunTool(): ToolRegistration {
63
63
  definition: {
64
64
  name: "op-run",
65
65
  description:
66
- "Submit an Op workflow to Temporal. The worker must already be running — start it first with `chant run <name>`.",
66
+ "Submit an Op workflow to Temporal (Temporal mode). A Temporal worker must already be running — start it with `chant run <name> --temporal`.",
67
67
  inputSchema: {
68
68
  type: "object",
69
69
  properties: {
@@ -1,7 +1,7 @@
1
1
  import { resolve } from "node:path";
2
2
  import type { ResourceDefinition } from "./types";
3
3
  import { getContext } from "./resources/context";
4
- import { readSnapshot, readEnvironmentSnapshots } from "../../state/git";
4
+ import { readSnapshot, readEnvironmentSnapshots } from "../../lifecycle/git";
5
5
  import { discoverOps } from "../../op/discover";
6
6
  import { makeTemporalClient } from "../handlers/run";
7
7
  import { resolveWorkflowId } from "../handlers/run-client";
@@ -1133,9 +1133,9 @@ describe("McpServer", () => {
1133
1133
  const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
1134
1134
  expect(tools).toHaveLength(13);
1135
1135
  expect(tools.map((t) => t.name).sort()).toEqual([
1136
- "build", "explain", "import", "lint",
1136
+ "build", "explain", "import", "lifecycle-diff", "lifecycle-snapshot", "lint",
1137
1137
  "op-list", "op-report", "op-run", "op-signal", "op-status",
1138
- "scaffold", "search", "state-diff", "state-snapshot",
1138
+ "scaffold", "search",
1139
1139
  ]);
1140
1140
 
1141
1141
  const resourcesRes = await s.handleRequest({ jsonrpc: "2.0", id: 3, method: "resources/list" });
@@ -8,7 +8,7 @@ import { scaffoldTool, createScaffoldHandler } from "./tools/scaffold";
8
8
  import { searchTool, createSearchHandler } from "./tools/search";
9
9
  import type { LexiconPlugin } from "../../lexicon";
10
10
  import type { McpRequest, McpResponse, ToolDefinition, ToolHandler, ResourceDefinition } from "./types";
11
- import { createSnapshotTool, createDiffTool } from "./state-tools";
11
+ import { createSnapshotTool, createDiffTool } from "./lifecycle-tools";
12
12
  import { createOpListTool, createOpRunTool, createOpStatusTool, createOpSignalTool, createOpReportTool } from "./op-tools";
13
13
  import { buildResourcesList, handleResourcesRead } from "./resource-handlers";
14
14
 
@@ -20,6 +20,12 @@ export interface ParsedArgs {
20
20
  help: boolean;
21
21
  profile?: string;
22
22
  report?: boolean;
23
+ /** `chant run` — force the local in-process executor (the default). */
24
+ local?: boolean;
25
+ /** `chant run` — run via a Temporal cluster instead of the local executor. */
26
+ temporal?: boolean;
27
+ /** `chant run` — emit the structured OpRunResult as JSON on stdout. */
28
+ json?: boolean;
23
29
  live: boolean;
24
30
  /** `chant migrate --from <name>` (default "github") */
25
31
  migrateFrom?: string;
@@ -37,6 +43,14 @@ export interface ParsedArgs {
37
43
  reportFile?: string;
38
44
  /** `chant init --skill <name>` filter (added in #95 commit) */
39
45
  skill?: string;
46
+ /** `chant import --type <ResourceType>` selector */
47
+ selectType?: string;
48
+ /** `chant import --name <name>` selector */
49
+ selectName?: string;
50
+ /** `chant import --owned` — restrict live import to chant-owned resources */
51
+ owned?: boolean;
52
+ /** `chant import --verbatim` — keep server-defaulted fields in live import */
53
+ verbatim?: boolean;
40
54
  }
41
55
 
42
56
  /**
@@ -75,16 +89,21 @@ export interface ResolvedCommand {
75
89
  * Supports compound commands like "dev generate" where args.command="dev"
76
90
  * and args.path="generate". Falls back to simple command matching.
77
91
  */
92
+ /** Short command aliases. `chant lc …` is sugar for `chant lifecycle …`. */
93
+ const COMMAND_ALIASES: Record<string, string> = { lc: "lifecycle" };
94
+
78
95
  export function resolveCommand(args: ParsedArgs, registry: CommandDef[]): ResolvedCommand | null {
96
+ const command = COMMAND_ALIASES[args.command] ?? args.command;
97
+
79
98
  // Try compound command first: "dev generate", "serve lsp", "init lexicon"
80
- const compound = `${args.command} ${args.path}`;
99
+ const compound = `${command} ${args.path}`;
81
100
  const compoundMatch = registry.find((c) => c.name === compound);
82
101
  if (compoundMatch) {
83
102
  return { def: compoundMatch, compound: true };
84
103
  }
85
104
 
86
105
  // Try simple command
87
- const simpleMatch = registry.find((c) => c.name === args.command);
106
+ const simpleMatch = registry.find((c) => c.name === command);
88
107
  if (simpleMatch) {
89
108
  return { def: simpleMatch, compound: false };
90
109
  }