@intentius/chant 0.124.23 → 0.124.25

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 (210) hide show
  1. package/dist/ci-jobs.d.ts +41 -1
  2. package/dist/ci-jobs.d.ts.map +1 -1
  3. package/dist/ci-jobs.js +48 -1
  4. package/dist/cli/commands/build.d.ts.map +1 -1
  5. package/dist/cli/commands/build.js +14 -0
  6. package/dist/cli/handlers/approve.d.ts.map +1 -1
  7. package/dist/cli/handlers/approve.js +9 -0
  8. package/dist/cli/handlers/emulator.d.ts +13 -2
  9. package/dist/cli/handlers/emulator.d.ts.map +1 -1
  10. package/dist/cli/handlers/emulator.js +63 -3
  11. package/dist/cli/handlers/promote.d.ts.map +1 -1
  12. package/dist/cli/handlers/promote.js +7 -1
  13. package/dist/cli/handlers/run-wave.d.ts.map +1 -1
  14. package/dist/cli/handlers/run-wave.js +5 -4
  15. package/dist/cli/handlers/run.d.ts +4 -3
  16. package/dist/cli/handlers/run.d.ts.map +1 -1
  17. package/dist/cli/handlers/run.js +73 -9
  18. package/dist/cli/main.d.ts.map +1 -1
  19. package/dist/cli/main.js +38 -11
  20. package/dist/cli/mcp/workspace-tools.d.ts +3 -3
  21. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  22. package/dist/cli/mcp/workspace-tools.js +27 -4
  23. package/dist/cli/parse-args.d.ts.map +1 -1
  24. package/dist/cli/parse-args.js +31 -0
  25. package/dist/cli/registry.d.ts +12 -0
  26. package/dist/cli/registry.d.ts.map +1 -1
  27. package/dist/components/cli-support.d.ts +45 -6
  28. package/dist/components/cli-support.d.ts.map +1 -1
  29. package/dist/components/cli-support.js +97 -22
  30. package/dist/components/driver.d.ts.map +1 -1
  31. package/dist/components/driver.js +25 -10
  32. package/dist/effect-receipt.d.ts +13 -0
  33. package/dist/effect-receipt.d.ts.map +1 -1
  34. package/dist/effect-receipt.js +19 -0
  35. package/dist/graph-detail.d.ts.map +1 -1
  36. package/dist/graph-detail.js +24 -2
  37. package/dist/lexicon-module.d.ts +7 -1
  38. package/dist/lexicon-module.d.ts.map +1 -1
  39. package/dist/lexicon-module.js +7 -2
  40. package/dist/lifecycle/git.d.ts +11 -0
  41. package/dist/lifecycle/git.d.ts.map +1 -1
  42. package/dist/lifecycle/git.js +62 -6
  43. package/dist/lifecycle/no-record.d.ts +59 -0
  44. package/dist/lifecycle/no-record.d.ts.map +1 -0
  45. package/dist/lifecycle/no-record.js +88 -0
  46. package/dist/lifecycle/receipt-plan.d.ts.map +1 -1
  47. package/dist/lifecycle/receipt-plan.js +4 -2
  48. package/dist/op/activities/shell.d.ts.map +1 -1
  49. package/dist/op/activities/shell.js +30 -9
  50. package/dist/op/emulator-lifecycle.d.ts +23 -0
  51. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  52. package/dist/op/emulator-lifecycle.js +39 -2
  53. package/dist/op/gate.d.ts.map +1 -1
  54. package/dist/op/gate.js +3 -0
  55. package/dist/op/index.d.ts +1 -1
  56. package/dist/op/index.d.ts.map +1 -1
  57. package/dist/op/index.js +1 -1
  58. package/dist/op/local-executor.d.ts +10 -0
  59. package/dist/op/local-executor.d.ts.map +1 -1
  60. package/dist/op/local-executor.js +71 -7
  61. package/dist/op/op-waves-notify.d.ts +2 -0
  62. package/dist/op/op-waves-notify.d.ts.map +1 -1
  63. package/dist/op/op-waves-notify.js +9 -3
  64. package/dist/op/op-waves-run.d.ts +8 -2
  65. package/dist/op/op-waves-run.d.ts.map +1 -1
  66. package/dist/op/op-waves-run.js +36 -11
  67. package/dist/op/op-waves.d.ts +85 -1
  68. package/dist/op/op-waves.d.ts.map +1 -1
  69. package/dist/op/op-waves.js +153 -1
  70. package/dist/op/receipt-store.d.ts +5 -0
  71. package/dist/op/receipt-store.d.ts.map +1 -1
  72. package/dist/op/receipt-store.js +4 -3
  73. package/dist/op/runtimes/local.d.ts.map +1 -1
  74. package/dist/op/runtimes/local.js +6 -0
  75. package/dist/op/step-console.d.ts +65 -0
  76. package/dist/op/step-console.d.ts.map +1 -0
  77. package/dist/op/step-console.js +140 -0
  78. package/dist/op/step-output-ref.d.ts +26 -18
  79. package/dist/op/step-output-ref.d.ts.map +1 -1
  80. package/dist/op/step-output-ref.js +86 -63
  81. package/dist/point-decision.d.ts.map +1 -1
  82. package/dist/point-decision.js +3 -0
  83. package/dist/wave-runtime.mjs +239 -239
  84. package/dist/workspace/agent-cli.d.ts.map +1 -1
  85. package/dist/workspace/agent-cli.js +3 -0
  86. package/dist/workspace/compose-graph.d.ts +22 -3
  87. package/dist/workspace/compose-graph.d.ts.map +1 -1
  88. package/dist/workspace/compose-graph.js +18 -5
  89. package/dist/workspace/conformance/__writer_fixture__/answers/answer.schema.json +16 -1
  90. package/dist/workspace/conformance/writer.js +1 -1
  91. package/dist/workspace/decide.d.ts +51 -9
  92. package/dist/workspace/decide.d.ts.map +1 -1
  93. package/dist/workspace/decide.js +125 -14
  94. package/dist/workspace/graph.schema.json +68 -1
  95. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  96. package/dist/workspace/lineage-upgrade.js +20 -11
  97. package/dist/workspace/nested-graph.d.ts +1 -1
  98. package/dist/workspace/nested-graph.d.ts.map +1 -1
  99. package/dist/workspace/nested-graph.js +5 -1
  100. package/dist/workspace/point-answer.schema.json +16 -1
  101. package/dist/workspace/points-cli.d.ts +5 -3
  102. package/dist/workspace/points-cli.d.ts.map +1 -1
  103. package/dist/workspace/points-cli.js +16 -7
  104. package/dist/workspace/points-write.schema.json +39 -4
  105. package/dist/workspace/points.schema.json +42 -3
  106. package/dist/workspace/reason-codes.d.ts +2 -1
  107. package/dist/workspace/reason-codes.d.ts.map +1 -1
  108. package/dist/workspace/reason-codes.js +2 -1
  109. package/dist/workspace/records-cli.d.ts +12 -2
  110. package/dist/workspace/records-cli.d.ts.map +1 -1
  111. package/dist/workspace/records-cli.js +27 -7
  112. package/dist/workspace/records-write.d.ts.map +1 -1
  113. package/dist/workspace/records-write.js +2 -3
  114. package/dist/workspace/records.d.ts +10 -4
  115. package/dist/workspace/records.d.ts.map +1 -1
  116. package/dist/workspace/records.js +16 -3
  117. package/dist/workspace/records.schema.json +2 -1
  118. package/dist/workspace/runs.d.ts.map +1 -1
  119. package/dist/workspace/runs.js +4 -2
  120. package/dist/workspace/trust/verify-cli.d.ts.map +1 -1
  121. package/dist/workspace/trust/verify-cli.js +2 -1
  122. package/dist/workspace/trust/verify.d.ts +22 -0
  123. package/dist/workspace/trust/verify.d.ts.map +1 -1
  124. package/dist/workspace/trust/verify.js +94 -14
  125. package/dist/workspace/write-scope.d.ts +3 -1
  126. package/dist/workspace/write-scope.d.ts.map +1 -1
  127. package/dist/workspace/write-scope.js +11 -4
  128. package/package.json +1 -1
  129. package/src/ci-jobs.ts +58 -1
  130. package/src/cli/commands/build.ts +18 -0
  131. package/src/cli/compiled-cli.e2e.test.ts +45 -1
  132. package/src/cli/handlers/approve.ts +9 -0
  133. package/src/cli/handlers/emulator.test.ts +61 -1
  134. package/src/cli/handlers/emulator.ts +65 -4
  135. package/src/cli/handlers/promote.test.ts +7 -0
  136. package/src/cli/handlers/promote.ts +7 -1
  137. package/src/cli/handlers/run-no-record.e2e.test.ts +87 -0
  138. package/src/cli/handlers/run-wave.ts +5 -4
  139. package/src/cli/handlers/run.test.ts +126 -0
  140. package/src/cli/handlers/run.ts +69 -9
  141. package/src/cli/main.test.ts +34 -0
  142. package/src/cli/main.ts +38 -11
  143. package/src/cli/mcp/workspace-tools.e2e.test.ts +1 -1
  144. package/src/cli/mcp/workspace-tools.ts +27 -4
  145. package/src/cli/parse-args.ts +21 -0
  146. package/src/cli/registry.ts +12 -0
  147. package/src/components/cli-support.test.ts +75 -0
  148. package/src/components/cli-support.ts +130 -25
  149. package/src/components/driver.test.ts +28 -0
  150. package/src/components/driver.ts +23 -10
  151. package/src/components/generate-disabled.test.ts +56 -0
  152. package/src/effect-receipt.ts +22 -0
  153. package/src/graph-ci-edges.test.ts +50 -0
  154. package/src/graph-detail.ts +21 -2
  155. package/src/lexicon-module.ts +9 -2
  156. package/src/lifecycle/git.ts +61 -6
  157. package/src/lifecycle/no-record.test.ts +175 -0
  158. package/src/lifecycle/no-record.ts +108 -0
  159. package/src/lifecycle/receipt-plan.test.ts +9 -0
  160. package/src/lifecycle/receipt-plan.ts +4 -1
  161. package/src/op/activities/shell.ts +28 -6
  162. package/src/op/effect-step.test.ts +95 -1
  163. package/src/op/emulator-lifecycle.test.ts +24 -1
  164. package/src/op/emulator-lifecycle.ts +58 -2
  165. package/src/op/gate.ts +3 -0
  166. package/src/op/index.ts +1 -1
  167. package/src/op/local-executor.test.ts +65 -0
  168. package/src/op/local-executor.ts +88 -8
  169. package/src/op/op-waves-jobs.test.ts +213 -0
  170. package/src/op/op-waves-notify.ts +11 -5
  171. package/src/op/op-waves-run.ts +42 -11
  172. package/src/op/op-waves.ts +202 -2
  173. package/src/op/receipt-store.ts +13 -3
  174. package/src/op/runtimes/local.ts +6 -0
  175. package/src/op/step-console.test.ts +150 -0
  176. package/src/op/step-console.ts +155 -0
  177. package/src/op/step-output-ref.test.ts +68 -24
  178. package/src/op/step-output-ref.ts +85 -65
  179. package/src/point-decision-bundle.test.ts +1 -0
  180. package/src/point-decision.ts +3 -0
  181. package/src/workspace/agent-cli.ts +2 -0
  182. package/src/workspace/compose-graph.test.ts +55 -0
  183. package/src/workspace/compose-graph.ts +28 -6
  184. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +16 -1
  185. package/src/workspace/conformance/writer.ts +1 -1
  186. package/src/workspace/decide.test.ts +87 -3
  187. package/src/workspace/decide.ts +142 -17
  188. package/src/workspace/graph-contract.test.ts +26 -1
  189. package/src/workspace/graph.schema.json +68 -1
  190. package/src/workspace/lineage-upgrade.test.ts +21 -0
  191. package/src/workspace/lineage-upgrade.ts +19 -11
  192. package/src/workspace/nested-graph.test.ts +14 -0
  193. package/src/workspace/nested-graph.ts +7 -2
  194. package/src/workspace/point-answer.schema.json +16 -1
  195. package/src/workspace/points-cli.ts +16 -8
  196. package/src/workspace/points-write.schema.json +39 -4
  197. package/src/workspace/points.schema.json +42 -3
  198. package/src/workspace/reason-codes.ts +2 -1
  199. package/src/workspace/record-assets.test.ts +14 -0
  200. package/src/workspace/records-cli.ts +28 -7
  201. package/src/workspace/records-write-commands.e2e.test.ts +32 -0
  202. package/src/workspace/records-write.ts +2 -3
  203. package/src/workspace/records.schema.json +2 -1
  204. package/src/workspace/records.ts +17 -4
  205. package/src/workspace/runs.ts +4 -2
  206. package/src/workspace/trust/trust.test.ts +105 -0
  207. package/src/workspace/trust/verify-cli.ts +2 -1
  208. package/src/workspace/trust/verify.ts +106 -13
  209. package/src/workspace/write-scope.test.ts +18 -0
  210. package/src/workspace/write-scope.ts +16 -5
@@ -39,6 +39,7 @@ vi.mock("../../config", async () => {
39
39
  });
40
40
 
41
41
  vi.mock("../../components/cli-support", () => ({
42
+ isComponentList: (selector: string) => selector.includes(","),
42
43
  resolveComponentTargets: (...args: unknown[]) => resolveTargetsMock(...args),
43
44
  }));
44
45
 
@@ -286,6 +287,12 @@ describe("chant components rollback", () => {
286
287
  expect(appendReleaseRecordMock.mock.calls[0][0]).not.toHaveProperty("promotedFrom");
287
288
  });
288
289
 
290
+ test("a comma list is refused: rollback acts on one component (#3950)", async () => {
291
+ expect(await runComponentsRollback(rb({ component: "api,web" }))).toBe(1);
292
+ expect(errors.join("\n")).toMatch(/--component takes one component name, not a list/);
293
+ expect(ran).toEqual([]);
294
+ });
295
+
289
296
  test("a digest the environment never recorded fails clearly", async () => {
290
297
  expect(await runComponentsRollback(rb({ digest: "sha256:elsewhere" }))).toBe(1);
291
298
  expect(errors.join("\n")).toMatch(/sha256:elsewhere is not recorded for "api" in "prod"/);
@@ -25,7 +25,7 @@ import {
25
25
  type ReleaseRecord,
26
26
  type ReleaseRecordInput,
27
27
  } from "../../lifecycle/release-ledger";
28
- import { resolveComponentTargets } from "../../components/cli-support";
28
+ import { isComponentList, resolveComponentTargets } from "../../components/cli-support";
29
29
  import { applyConfigDefaults } from "../../components/config-defaults";
30
30
  import { fanOutRegistry } from "../../components/fan-out-support";
31
31
  import { renderDriverHuman } from "../../components/driver-output";
@@ -107,6 +107,12 @@ async function loadProject(ctx: CommandContext, selector: string) {
107
107
  for (const message of paramsResolution.errors) console.error(message);
108
108
  return 1;
109
109
  }
110
+ // A comma list is `chant run --components`' selector (#3950), which pulls in
111
+ // dependencies; promote, rollback and redeploy act on the one named component.
112
+ if (isComponentList(selector)) {
113
+ console.error(formatError({ message: `--component takes one component name, not a list ("${selector}")` }));
114
+ return 1;
115
+ }
110
116
  const targets = await resolveComponentTargets(projectPath, selector, args.sandbox, paramsResolution.provenance);
111
117
  if (!targets.success) {
112
118
  console.error(formatError({ message: targets.error ?? "Could not discover components" }));
@@ -0,0 +1,87 @@
1
+ /**
2
+ * #3955 — `chant run <op> --no-record` through the real CLI: the run executes,
3
+ * says at its start and end that it recorded nothing, and leaves
4
+ * `chant/lifecycle` where it was, both in the checkout and on origin. A plain
5
+ * run beside it records, so the unchanged ref is the flag's doing.
6
+ */
7
+ import { spawnSync } from "node:child_process";
8
+ import { join } from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
11
+ import { cleanScratch, commitAll, git, repo, REPO } from "../../workspace/__fixtures__/contract-repo";
12
+ import { pushLifecycleStatus, writeBlobToPath } from "../../lifecycle/git";
13
+
14
+ const MAIN = join(REPO, "packages/core/src/cli/main.ts");
15
+ const LOADER = pathToFileURL(join(REPO, "node_modules/tsx/dist/loader.mjs")).href;
16
+
17
+ /** An Op entity as `Op()` exports it, written as a literal so the fixture needs no import to resolve. */
18
+ function opModule(name: string, steps: unknown[]): string {
19
+ return `export const op = ${JSON.stringify({ entityType: "Chant::Op", props: { name, overview: name, phases: [{ name: "Main", steps }] } })};\n`;
20
+ }
21
+
22
+ const SHELL = { kind: "activity", fn: "shellCmd", profile: "atMostOnce", args: { cmd: "echo ran" } };
23
+
24
+ let dir: string;
25
+ let remote: string;
26
+
27
+ beforeAll(async () => {
28
+ remote = repo({}, false);
29
+ git(remote, "config", "core.bare", "true");
30
+ dir = repo(
31
+ {
32
+ "chant.config.ts": "export default { lexicons: [] };\n",
33
+ "ops/hello.op.ts": opModule("hello", [SHELL]),
34
+ "ops/gated.op.ts": opModule("gated", [{ kind: "gate", gate: "approve-prod" }, SHELL]),
35
+ },
36
+ false,
37
+ );
38
+ git(dir, "config", "user.name", "Run Test");
39
+ git(dir, "config", "user.email", "run@example.com");
40
+ commitAll(dir, "c0");
41
+ git(dir, "remote", "add", "origin", join(remote, ".git"));
42
+ await writeBlobToPath("seed", "seed.json", "{}\n", "seed", { cwd: dir });
43
+ expect((await pushLifecycleStatus({ cwd: dir })).status).toBe("pushed");
44
+ });
45
+ afterAll(cleanScratch);
46
+
47
+ function chant(args: string[], env: Record<string, string> = {}): { status: number | null; stderr: string } {
48
+ const r = spawnSync(process.execPath, ["--import", LOADER, MAIN, ...args], {
49
+ cwd: dir,
50
+ encoding: "utf8",
51
+ env: { ...process.env, NO_COLOR: "1", TSX_DISABLE_CACHE: "1", CHANT_NO_RECORD: "", ...env },
52
+ });
53
+ return { status: r.status, stderr: r.stderr };
54
+ }
55
+
56
+ function ledgerRefs(): { local: string; origin: string } {
57
+ return { local: git(dir, "rev-parse", "refs/heads/chant/lifecycle"), origin: git(dir, "ls-remote", "origin", "refs/heads/chant/lifecycle").split(/\s/)[0]! };
58
+ }
59
+
60
+ describe("chant run --no-record (#3955)", () => {
61
+ test("runs the Op, says so at the start and the end, and leaves the ledger ref unchanged here and on origin", () => {
62
+ const before = ledgerRefs();
63
+ const r = chant(["run", "hello", "--no-record"]);
64
+ expect(r.status, r.stderr).toBe(0);
65
+ expect(r.stderr).toContain("not recording (--no-record): nothing this run writes goes on chant/lifecycle");
66
+ expect(r.stderr).toMatch(/not recorded \(--no-record\): nothing went on chant\/lifecycle and nothing was pushed; 1 ledger write was dropped \(1 file\)/);
67
+ expect(ledgerRefs()).toEqual(before);
68
+ });
69
+
70
+ test("CHANT_NO_RECORD=1 fails a gate without writing a pending fact", () => {
71
+ const before = ledgerRefs();
72
+ const r = chant(["run", "gated"], { CHANT_NO_RECORD: "1" });
73
+ expect(r.status).toBe(1);
74
+ expect(r.stderr).toContain('gate "approve-prod" of "gated" cannot be passed under --no-record');
75
+ expect(r.stderr).toContain("not recorded (CHANT_NO_RECORD=1)");
76
+ expect(ledgerRefs()).toEqual(before);
77
+ });
78
+
79
+ test("a plain run of the same Op records its run", () => {
80
+ const before = ledgerRefs();
81
+ const r = chant(["run", "hello"]);
82
+ expect(r.status, r.stderr).toBe(0);
83
+ expect(r.stderr).not.toContain("not recording");
84
+ // The run record moves the local branch; it is pushed by the next write that pushes.
85
+ expect(ledgerRefs().local).not.toBe(before.local);
86
+ });
87
+ });
@@ -29,16 +29,16 @@ export async function runOpWaveCommand(ctx: CommandContext): Promise<number> {
29
29
  if (!specFile || args.wave === undefined) {
30
30
  console.error(formatError({
31
31
  message: "`chant run wave` needs --spec <file> and --wave <k>",
32
- hint: "Run it as a generated Op waves pipeline does: chant run wave --spec <file> --wave <k> [--decide | --share <i>]",
32
+ hint: "Run it as a generated Op waves pipeline does: chant run wave --spec <file> --wave <k> [--decide | --share <i> | --group <name> [--share <i>]]",
33
33
  }));
34
34
  return 1;
35
35
  }
36
- if (args.decide && args.share !== undefined) {
37
- console.error(formatError({ message: "--decide and --share are two different jobs; pass one" }));
36
+ if (args.decide && (args.share !== undefined || args.group !== undefined)) {
37
+ console.error(formatError({ message: `--decide and --${args.share !== undefined ? "share" : "group"} are two different jobs; pass one` }));
38
38
  return 1;
39
39
  }
40
40
  let spec: OpWavesSpec | undefined;
41
- const job = args.share !== undefined ? { share: args.share } : {};
41
+ const job = { ...(args.share !== undefined ? { share: args.share } : {}), ...(args.group !== undefined ? { group: args.group } : {}) };
42
42
  try {
43
43
  const path = resolve(specFile);
44
44
  spec = parseOpWavesSpec(readFileSync(path, "utf-8"), `--spec ${specFile}`);
@@ -48,6 +48,7 @@ export async function runOpWaveCommand(ctx: CommandContext): Promise<number> {
48
48
  wave: args.wave,
49
49
  ...(args.decide ? { decide: true } : {}),
50
50
  ...(args.share !== undefined ? { share: args.share } : {}),
51
+ ...(args.group !== undefined ? { group: args.group } : {}),
51
52
  ...(args.base ? { base: args.base } : {}),
52
53
  });
53
54
  if (args.json) console.log(JSON.stringify(result, null, 2));
@@ -61,6 +61,7 @@ vi.mock("node:fs", async () => {
61
61
  };
62
62
  });
63
63
  vi.mock("../../components/cli-support", () => ({
64
+ isComponentList: (selector: string) => selector.includes(","),
64
65
  runComponents: (...args: unknown[]) => runComponentsMock(...args),
65
66
  listComponents: (...args: unknown[]) => listComponentsMock(...args),
66
67
  }));
@@ -1277,6 +1278,25 @@ describe("runOp dispatcher: --components routes to runOpComponents", () => {
1277
1278
  vi.restoreAllMocks();
1278
1279
  });
1279
1280
 
1281
+ test("a list selector prints the resolved order and the added dependencies before running (#3950)", async () => {
1282
+ discoverOpsMock.mockReset();
1283
+ const stderr = makeStderrSpy();
1284
+ let printedBeforeRun = "";
1285
+ runComponentsMock.mockImplementation(async (_path: string, _selector: string, options: { onPlan?: (plan: unknown) => void }) => {
1286
+ options.onPlan?.({ order: ["db", "api", "web"], waves: [["db"], ["api", "web"]], added: ["db"] });
1287
+ printedBeforeRun = stderr.join("\n");
1288
+ return { success: true, selected: ["db", "api", "web"], run: { order: ["db", "api", "web"], waves: [["db"], ["api", "web"]], results: [], ok: true, status: "ok" } };
1289
+ });
1290
+
1291
+ const exit = await runOp({ args: makeArgs({ path: "api,web", components: true }), plugins: [], serializers: [] });
1292
+
1293
+ expect(exit).toBe(0);
1294
+ expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "api,web", expect.objectContaining({ onPlan: expect.any(Function) }));
1295
+ expect(printedBeforeRun).toContain("run order: db, then api + web");
1296
+ expect(printedBeforeRun).toContain("added as dependencies: db");
1297
+ vi.restoreAllMocks();
1298
+ });
1299
+
1280
1300
  // chant #1116 — the component driver never checked --report, so it was
1281
1301
  // silently ignored and the command fell through to a real dispatch.
1282
1302
  // Hard-error instead, before runComponents is ever reached.
@@ -1929,3 +1949,109 @@ describe("runOp: --fail-on-drift (#3675)", () => {
1929
1949
  expect(await runOp({ args: makeArgs({ path: "watch" }), plugins: [], serializers: [] })).toBe(2);
1930
1950
  });
1931
1951
  });
1952
+
1953
+ /**
1954
+ * chant #3955 — `--no-record` runs every step and records nothing on
1955
+ * `chant/lifecycle`. The run says so before it starts and after it ends, a
1956
+ * gate fails rather than recording a pending fact nobody could approve, and
1957
+ * a runtime that would record elsewhere is refused.
1958
+ */
1959
+ describe("runOp --no-record (#3955)", () => {
1960
+ const START = "not recording (--no-record)";
1961
+ const END = "not recorded (--no-record): nothing went on chant/lifecycle and nothing was pushed";
1962
+
1963
+ beforeEach(() => {
1964
+ discoverOpsMock.mockReset();
1965
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
1966
+ loadPluginsMock.mockReset().mockResolvedValue([]);
1967
+ runComponentsMock.mockReset();
1968
+ maybeRecordAutoReleaseMock.mockReset();
1969
+ maybePersistBuildManifestMock.mockReset().mockResolvedValue({ persisted: false, reason: "no-manifest" });
1970
+ gateLedger = memoryGateLedgerPort();
1971
+ delete process.env.CHANT_NO_RECORD;
1972
+ });
1973
+
1974
+ afterEach(() => {
1975
+ delete process.env.CHANT_NO_RECORD;
1976
+ vi.restoreAllMocks();
1977
+ });
1978
+
1979
+ test("an Op runs, the run says at its start and end that nothing was recorded, and the flag does not outlive it", async () => {
1980
+ let during: string | undefined;
1981
+ discoverOpsMock.mockImplementation(async () => {
1982
+ during = process.env.CHANT_NO_RECORD;
1983
+ return { ops: new Map([localOp("hello", [{ kind: "activity", fn: "shellCmd", profile: "atMostOnce", args: { cmd: "true" } }])]), errors: [] };
1984
+ });
1985
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1986
+ const stderr = makeStderrSpy();
1987
+
1988
+ const exit = await runOp({ args: makeArgs({ path: "hello", noRecord: true }), plugins: [], serializers: [] });
1989
+
1990
+ expect(exit).toBe(0);
1991
+ expect(during).toBe("1");
1992
+ expect(process.env.CHANT_NO_RECORD).toBeUndefined();
1993
+ expect(stderr[0]).toContain(START);
1994
+ expect(stderr.at(-1)).toContain(END);
1995
+ });
1996
+
1997
+ test("a gate fails the run and leaves no pending fact, and the end line still prints", async () => {
1998
+ discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("gated", [{ kind: "gate", gate: "approve-prod" }])]), errors: [] });
1999
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
2000
+ const stderr = makeStderrSpy();
2001
+
2002
+ const exit = await runOp({ args: makeArgs({ path: "gated", noRecord: true, gatedExit: 0 }), plugins: [], serializers: [] });
2003
+
2004
+ // A failure, not a gated stop: --gated-exit cannot turn it green.
2005
+ expect(exit).toBe(1);
2006
+ expect(gateLedger.appended).toHaveLength(0);
2007
+ expect(stderr.at(-1)).toContain(END);
2008
+ });
2009
+
2010
+ test("CHANT_NO_RECORD does the same without the flag, names itself, and stays set", async () => {
2011
+ process.env.CHANT_NO_RECORD = "1";
2012
+ discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("hello", [])]), errors: [] });
2013
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
2014
+ const stderr = makeStderrSpy();
2015
+
2016
+ const exit = await runOp({ args: makeArgs({ path: "hello" }), plugins: [], serializers: [] });
2017
+
2018
+ expect(exit).toBe(0);
2019
+ expect(stderr[0]).toContain("not recording (CHANT_NO_RECORD=1)");
2020
+ expect(process.env.CHANT_NO_RECORD).toBe("1");
2021
+ });
2022
+
2023
+ test("a runtime other than local is refused before the run starts", async () => {
2024
+ const start = vi.fn();
2025
+ loadChantConfigMock.mockResolvedValue({ config: { lexicons: ["stub"] } });
2026
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
2027
+ const stderr = makeStderrSpy();
2028
+
2029
+ const exit = await runOp({
2030
+ args: makeArgs({ path: "hello", on: "stub", noRecord: true }),
2031
+ plugins: [{ name: "stub", opRuntime: { name: "stub", start } } as never], serializers: [],
2032
+ });
2033
+
2034
+ expect(exit).toBe(1);
2035
+ expect(start).not.toHaveBeenCalled();
2036
+ expect(stderr.join("\n")).toContain('--no-record runs on the local runtime; the "stub" runtime records the run where it executes it');
2037
+ });
2038
+
2039
+ test("--components records no release and persists no manifest", async () => {
2040
+ runComponentsMock.mockResolvedValue({
2041
+ success: true,
2042
+ selected: ["svc"],
2043
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
2044
+ });
2045
+ maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "opted-out" });
2046
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
2047
+ const stderr = makeStderrSpy();
2048
+
2049
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", noRecord: true }), plugins: [], serializers: [] });
2050
+
2051
+ expect(exit).toBe(0);
2052
+ expect(maybeRecordAutoReleaseMock.mock.calls[0][1]).toMatchObject({ disabled: true });
2053
+ expect(maybePersistBuildManifestMock.mock.calls[0][1]).toMatchObject({ disabled: true });
2054
+ expect(stderr[0]).toContain(START);
2055
+ expect(stderr.at(-1)).toContain(END);
2056
+ });
2057
+ });
@@ -23,11 +23,13 @@ import { acquireStewardTurn, STEWARD_TURN_WAIT_MS } from "../../op/operator";
23
23
  import { releaseLease, currentHolderId, type AcquireLeaseResult } from "../../lifecycle/lease";
24
24
  import { StaleLockError } from "../../lifecycle/git";
25
25
  import { LIFECYCLE_LOCAL_NOTE, isLifecycleLocalNote } from "../../lifecycle/local-note";
26
+ import { ledgerRecordingOff, noRecordEndNote, noRecordStartNote, resetNoRecordLedger, NO_RECORD_ENV } from "../../lifecycle/no-record";
26
27
  import { renderDriverHuman, renderDriverJson } from "../../components/driver-output";
27
28
  import { ndjsonProgressSink } from "../../components/run-progress";
28
29
  import { maybeRecordAutoRelease } from "../../components/auto-release";
29
30
  import { maybePersistBuildManifest } from "../../components/manifest-persistence";
30
31
  import type { DriverComponentResult } from "../../components/driver";
32
+ import { isComponentList, type ComponentRunPlan } from "../../components/cli-support";
31
33
  import type { ReleaseRecord } from "../../lifecycle/release-ledger";
32
34
  import { runOpGenerate } from "./run-generate";
33
35
 
@@ -873,12 +875,13 @@ export async function recordAutoReleasesForRun(
873
875
  return recorded;
874
876
  }
875
877
 
876
- // ── chant run --components <name|all> ────────────────────────────────────────
878
+ // ── chant run --components <name|all|a,b,c> ──────────────────────────────────
877
879
 
878
880
  /**
879
- * `chant run --components <name|all> [--env <env>]` (#585) — the interpret
880
- * driver's CLI entrypoint, and since #2116 the only component run path there
881
- * is. `args.path` is the component name (or `all`), matching `chant run
881
+ * `chant run --components <name|all|a,b,c> [--env <env>]` (#585) — the
882
+ * interpret driver's CLI entrypoint, and since #2116 the only component run
883
+ * path there is. `args.path` is the component name (or `all`, or a comma list
884
+ * that also runs what the named components depend on, #3950), matching `chant run
882
885
  * <name>`'s Op-dispatch convention exactly (`args.path` is the Op name there
883
886
  * too) rather than a project directory — components are always discovered from
884
887
  * the current working directory, the same way Op discovery (`discoverOps()`)
@@ -909,11 +912,52 @@ export async function recordAutoReleasesForRun(
909
912
  * auto-release check below used to apply itself) and reused for that check,
910
913
  * rather than loaded a second time.
911
914
  */
912
- export async function runOpComponents(ctx: CommandContext): Promise<number> {
915
+ export function runOpComponents(ctx: CommandContext): Promise<number> {
916
+ return withRecordingChoice(ctx, () => runComponentSelection(ctx));
917
+ }
918
+
919
+ /**
920
+ * Run `run` with recording off when `--no-record` or `CHANT_NO_RECORD` asks
921
+ * for it (#3955), and say so before it starts and after it ends, whatever
922
+ * the outcome. The flag sets `CHANT_NO_RECORD` for the length of the run, so
923
+ * the ledger seam (`../../lifecycle/git.ts`) and every chant process a step
924
+ * starts see the same choice. A run with recording on is untouched.
925
+ */
926
+ async function withRecordingChoice(ctx: CommandContext, run: () => Promise<number>): Promise<number> {
927
+ const flagged = ctx.args.noRecord === true;
928
+ if (!flagged && !ledgerRecordingOff()) return run();
929
+ const source = flagged ? "--no-record" : `${NO_RECORD_ENV}=${process.env[NO_RECORD_ENV]}`;
930
+ const prior = process.env[NO_RECORD_ENV];
931
+ if (flagged) process.env[NO_RECORD_ENV] = "1";
932
+ resetNoRecordLedger();
933
+ console.error(formatWarning({ message: noRecordStartNote(source) }));
934
+ try {
935
+ return await run();
936
+ } finally {
937
+ console.error(formatWarning({ message: noRecordEndNote(source) }));
938
+ resetNoRecordLedger();
939
+ if (flagged) {
940
+ if (prior === undefined) delete process.env[NO_RECORD_ENV];
941
+ else process.env[NO_RECORD_ENV] = prior;
942
+ }
943
+ }
944
+ }
945
+
946
+ /** A run that records nothing stays in this process (#3955): a runtime that runs it elsewhere would record there. */
947
+ function refusesNoRecordOn(runtime: Pick<OpRuntimeProvider, "name">): boolean {
948
+ if (!ledgerRecordingOff() || runtime.name === "local") return false;
949
+ console.error(formatError({
950
+ message: `--no-record runs on the local runtime; the "${runtime.name}" runtime records the run where it executes it`,
951
+ hint: `Omit --on to run it here without recording, or drop --no-record (and ${NO_RECORD_ENV}).`,
952
+ }));
953
+ return true;
954
+ }
955
+
956
+ async function runComponentSelection(ctx: CommandContext): Promise<number> {
913
957
  const selector = ctx.args.path;
914
958
  if (!selector || selector === ".") {
915
959
  console.error(formatError({
916
- message: "Component name is required: chant run --components <name|all>",
960
+ message: "Component name is required: chant run --components <name|all|a,b,c>",
917
961
  hint: "Run `chant list --components` to see available components.",
918
962
  }));
919
963
  return 1;
@@ -936,6 +980,7 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
936
980
 
937
981
  const runtime = await resolveOpRuntime(ctx);
938
982
  if (!runtime) return 1;
983
+ if (refusesNoRecordOn(runtime)) return 1;
939
984
  if (!runtime.runComponents) {
940
985
  console.error(formatError({
941
986
  message: `--components is not supported on the "${runtime.name}" runtime`,
@@ -966,12 +1011,22 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
966
1011
  // `undefined` and every `onProgress?.(...)` call in the driver is a no-op —
967
1012
  // behavior is byte-for-byte unchanged from before this flag existed.
968
1013
  const onProgress = ctx.args.progressJson ? ndjsonProgressSink() : undefined;
1014
+ // A list (#3950) pulls in what its components depend on; say which ones and
1015
+ // in what order before the first one runs. stderr, so --json and
1016
+ // --progress-json keep stdout to themselves.
1017
+ const onPlan = isComponentList(selector)
1018
+ ? (plan: ComponentRunPlan) => {
1019
+ console.error(formatInfo(`run order: ${plan.waves.map((wave) => wave.join(" + ")).join(", then ")}`));
1020
+ if (plan.added.length > 0) console.error(formatInfo(`added as dependencies: ${plan.added.join(", ")}`));
1021
+ }
1022
+ : undefined;
969
1023
  const result = await runtime.runComponents(projectPath, selector, {
970
1024
  env: ctx.args.env,
971
1025
  componentOutputs: seededOutputs,
972
1026
  onProgress,
973
1027
  sandbox: ctx.args.sandbox,
974
1028
  buildParams: paramsResolution.provenance,
1029
+ ...(onPlan ? { onPlan } : {}),
975
1030
  });
976
1031
 
977
1032
  // Dump the accumulated outputs for a downstream job to seed from. Written
@@ -1037,7 +1092,8 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
1037
1092
  }
1038
1093
 
1039
1094
  if (result.success && result.run) {
1040
- const disabled = resolveAutoReleaseDisabled(config, ctx.args.noReleaseRecord);
1095
+ // #3955: a run that records nothing has no release to report either.
1096
+ const disabled = resolveAutoReleaseDisabled(config, ctx.args.noReleaseRecord) || ledgerRecordingOff();
1041
1097
  const recorded = await recordAutoReleasesForRun(result.run.results, env, `local-${Date.now()}`, disabled);
1042
1098
  // `--digest-file` (#2602): the releases this run recorded, one
1043
1099
  // `<component>=<digest>` line each, the form `chant components promote
@@ -1065,7 +1121,11 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
1065
1121
  * what they were before the seam existed; a runtime that reports only a state
1066
1122
  * gets the terser render instead.
1067
1123
  */
1068
- export async function runOpOnRuntime(ctx: CommandContext): Promise<number> {
1124
+ export function runOpOnRuntime(ctx: CommandContext): Promise<number> {
1125
+ return withRecordingChoice(ctx, () => runOpOnResolvedRuntime(ctx));
1126
+ }
1127
+
1128
+ async function runOpOnResolvedRuntime(ctx: CommandContext): Promise<number> {
1069
1129
  const opName = ctx.args.path;
1070
1130
  if (!opName || opName === ".") {
1071
1131
  console.error(formatError({
@@ -1108,7 +1168,7 @@ export async function runOpOnRuntime(ctx: CommandContext): Promise<number> {
1108
1168
  }
1109
1169
 
1110
1170
  const runtime = await resolveOpRuntime(ctx);
1111
- if (!runtime) {
1171
+ if (!runtime || refusesNoRecordOn(runtime)) {
1112
1172
  await stewardGate?.release?.();
1113
1173
  return 1;
1114
1174
  }
@@ -361,6 +361,22 @@ describe("parseArgs", () => {
361
361
 
362
362
  // ── --no-release-record (#597) ───────────────────────────────────────────
363
363
 
364
+ test("emulator up --image/--network/--setting/--loopback parse, and a missing value is refused (#3952)", () => {
365
+ const a = parseArgs([
366
+ "emulator", "up", "--image", "floci/floci:1.6.0", "--network", "loom",
367
+ "--setting", "FLOCI_A=1", "--setting", "FLOCI_B=2", "--loopback",
368
+ ]);
369
+ expect(a.image).toBe("floci/floci:1.6.0");
370
+ expect(a.network).toBe("loom");
371
+ expect(a.setting).toEqual(["FLOCI_A=1", "FLOCI_B=2"]);
372
+ expect(a.loopback).toBe(true);
373
+ expect(parseArgs(["emulator", "up", "--setting=FLOCI_A=1"]).setting).toEqual(["FLOCI_A=1"]);
374
+ expect(() => parseArgs(["emulator", "up", "--image"])).toThrow(/--image needs an image reference/);
375
+ expect(() => parseArgs(["emulator", "up", "--network", "--json"])).toThrow(/--network needs a Docker network name/);
376
+ expect(() => parseArgs(["emulator", "up", "--setting"])).toThrow(/--setting needs a value/);
377
+ expect(() => parseArgs(["emulator", "up", "--loopback=yes"])).toThrow();
378
+ });
379
+
364
380
  test("--promote-to <env> sets promoteTo, and a missing value is refused (#2575)", () => {
365
381
  expect(parseArgs(["build", "--components", "--generate", "github", "--promote-to", "prod"]).promoteTo).toBe("prod");
366
382
  expect(() => parseArgs(["build", "--components", "--generate", "github", "--promote-to"])).toThrow(/--promote-to needs an environment/);
@@ -390,6 +406,14 @@ describe("parseArgs", () => {
390
406
  expect(result.noReleaseRecord).toBeUndefined();
391
407
  });
392
408
 
409
+ test("parses --no-record for run <op> and run --components, apart from --no-release-record (#3955)", () => {
410
+ expect(parseArgs(["run", "smoke", "--no-record"])).toMatchObject({ path: "smoke", noRecord: true });
411
+ const components = parseArgs(["run", "--components", "svc", "--no-record"]);
412
+ expect(components).toMatchObject({ components: true, noRecord: true });
413
+ expect(components.noReleaseRecord).toBeUndefined();
414
+ expect(parseArgs(["run", "smoke"]).noRecord).toBeUndefined();
415
+ });
416
+
393
417
  // ── --progress-json (M3, behold roadmap) ─────────────────────────────────
394
418
 
395
419
  test("parses --progress-json for run --components", () => {
@@ -979,3 +1003,13 @@ describe("chant workspace check --generated (#2641)", () => {
979
1003
  expect(() => parseArgs(["workspace", "check", "--generated=yes"])).toThrow();
980
1004
  });
981
1005
  });
1006
+
1007
+ describe("chant --help", () => {
1008
+ test("lists every top-level command in the registry", () => {
1009
+ const source = readFileSync(join(import.meta.dirname, "main.ts"), "utf-8");
1010
+ const help = source.slice(source.indexOf("function printHelp("), source.indexOf("Options:", source.indexOf("function printHelp(")));
1011
+ const listed = new Set([...help.matchAll(/^ {2}([a-z][a-z-]*)\b/gm)].map((m) => m[1]));
1012
+ const commands = new Set(commandRegistry.map((def) => def.name.split(" ")[0]));
1013
+ expect([...commands].filter((name) => !listed.has(name))).toEqual([]);
1014
+ });
1015
+ });
package/src/cli/main.ts CHANGED
@@ -167,7 +167,12 @@ Commands:
167
167
  okf emits an OKF v0.2 knowledge bundle — one markdown
168
168
  concept per entity + index.md; -o <dir> writes the
169
169
  bundle tree, otherwise JSON path→content on stdout)
170
- generate Generate typed code from sources chant.config declares
170
+ search "<query>" Answer an estate question with a few matching rows
171
+ instead of a graph dump (kind:, tag:, attr:, ->/<- edge
172
+ terms; --live or --at <ref> with --env <name> reads the
173
+ estate or a recorded snapshot; --show <attr>,
174
+ --check-live, --check-snapshot, --fail-on-drift)
175
+ generate Generate typed code from sources chant.config declares
171
176
  (k8s.crds, helm.charts) into src/generated/<lexicon>/
172
177
  (codegen.outDir); --check exits 1 when the committed
173
178
  output no longer matches its sources, as build does
@@ -253,6 +258,8 @@ Ops:
253
258
  with a work lease runs under, and who holds it
254
259
  --fail-on-drift: exit 2 when the run completes and
255
260
  its Drift outcome is true (WatchOp failOn: "drift")
261
+ --no-record: a test run that records nothing on
262
+ chant/lifecycle and pushes nothing (local runtime)
256
263
  run list List all Ops with the runtime's state for each
257
264
  run status <name> Show the runtime's state for one Op's latest run
258
265
  run approve <op> <gate> Record a gate's resolution and wake the runtime
@@ -261,7 +268,8 @@ Ops:
261
268
  run wave --spec <file> --wave <k> Plan, gate and apply wave k of an Op
262
269
  waves spec (#3679): the job a generated waves
263
270
  pipeline runs. --decide plans and decides only;
264
- --share <i> applies one share of a decided wave.
271
+ --share <i> applies one share of a decided wave;
272
+ --group <name> applies one job group's runs.
265
273
  --record-plans (no --wave), in a pull request's
266
274
  pipeline: plan every wave at its head and record the
267
275
  digests a pr-review approval mode compares
@@ -278,8 +286,11 @@ Ops:
278
286
  declares a schedule or from --spec <file.json>.
279
287
  Files go to the forge's own directory unless
280
288
  --output <dir>; --format json prints them instead
281
- run --components <name|all> Run discovered Component(s) through the interpret
282
- driver on the local executor (--env <env>; #585).
289
+ run --components <name|all|a,b,c> Run discovered Component(s) through the
290
+ interpret driver on the local executor (--env <env>;
291
+ #585). A comma list also runs what the named
292
+ components depend on, in dependency order, in one
293
+ run, and prints that order first (#3950).
283
294
  On success, auto-emits a release-ledger record per
284
295
  component that published a digest (default: on;
285
296
  --no-release-record to opt out; #597)
@@ -396,9 +407,11 @@ Workspace (level 1, #2524):
396
407
  state transitions, new verdicts, new supersessions
397
408
  and changed pins. A session id compares the commits
398
409
  the session opened and closed at
399
- workspace records pin <path>
410
+ workspace records pin <path> [--kind <kind file>]
400
411
  Print the path from the workspace root and the
401
- sha256 of a file, for a decision's evidence pin
412
+ sha256 of a file, for a decision's evidence pin. The
413
+ root is the kind file's workspace with --kind, else
414
+ the workspace above the current directory
402
415
  workspace records new [<kind file>] --from <file|-> [--prefix <prefix>] [--sign [<key file>]] [--dry-run]
403
416
  Write one new record in the kind's directory from
404
417
  the JSON fields given, after validating them as
@@ -553,6 +566,11 @@ Workspace (level 1, #2524):
553
566
  workspace points retract <id> --by <name>... [--note <text>] [--kind <kind file>] [--dry-run]
554
567
  Take an answer back: the question is open for people
555
568
  again, and the answer stays in its retractions
569
+ workspace points withdraw <id> --by <name>... [--note <text>] [--kind <kind file>] [--dry-run]
570
+ Close an open question that no longer applies, such as
571
+ one about a work item its author changed: points --open
572
+ leaves it out, nobody answers it, and asking it again
573
+ opens it again (#2888)
556
574
  workspace pin <path> [--json]
557
575
  Print the integrity value that pins the plugin at
558
576
  <path>, to put in a path pin of chant.workspace.json.
@@ -807,7 +825,8 @@ Lexicon development:
807
825
  Local:
808
826
  emulator <up|down|status> Boot/stop/inspect configured lexicons' local
809
827
  emulators (Floci etc.); --lexicon <name>, --json,
810
- --port; \`chant emulator --help\` lists ports and logins
828
+ --port, --image, --network, --setting, --loopback;
829
+ \`chant emulator --help\` lists ports and logins
811
830
 
812
831
  Servers:
813
832
  serve lsp Start the LSP server (stdio)
@@ -880,6 +899,12 @@ Options:
880
899
  successful \`run --components\` deploy (default: on;
881
900
  also settable via chant.config.ts's
882
901
  release.autoRecord: false; #597)
902
+ --no-record (run <op>, run --components) Run every step but record
903
+ nothing on chant/lifecycle and push nothing: no
904
+ release, run record, receipt or gate fact. A gate
905
+ step fails. The run says so at its start and end.
906
+ CHANT_NO_RECORD=1 does the same for a whole CI job
907
+ (#3955)
883
908
  --fold (build) Fold source modules statically instead of
884
909
  running them; folds resource constructors and
885
910
  composite factory calls (#1022/#1023), falling back
@@ -1213,16 +1238,18 @@ export const commandRegistry: CommandDef[] = [
1213
1238
 
1214
1239
  // Workspace reads (#2524). Imported on first use, so a level-0 command never
1215
1240
  // loads anything under workspace/ (#2525 rule 5, pinned by #2526's goldens).
1216
- { name: "workspace records", handler: async (ctx) => (await import("../workspace/records-cli")).runWorkspaceRecords(ctx) },
1241
+ // #3276 — records, points, ls and status read the declaration and never the
1242
+ // config of the member they run in, so they skip its load (and tsx with it).
1243
+ { name: "workspace records", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/records-cli")).runWorkspaceRecords(ctx) },
1217
1244
  // ws-058 (#2739) — decision points and the questions asked of them; never calls a model.
1218
- { name: "workspace points", handler: async (ctx) => (await import("../workspace/points-cli")).runWorkspacePoints(ctx) },
1245
+ { name: "workspace points", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/points-cli")).runWorkspacePoints(ctx) },
1219
1246
  { name: "workspace init", handler: async (ctx) => (await import("../workspace/init")).runWorkspaceInit(ctx) },
1220
- { name: "workspace ls", handler: async (ctx) => (await import("../workspace/ls")).runWorkspaceLs(ctx) },
1247
+ { name: "workspace ls", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ls")).runWorkspaceLs(ctx) },
1221
1248
  // #2732 — the work lease: claim, renew and release a work item.
1222
1249
  { name: "workspace work", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/work-cli")).runWorkspaceWork(ctx) },
1223
1250
  // #3033 — the agent run record: what each run cost and which commits it made. chant records runs and never starts one.
1224
1251
  { name: "workspace runs", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/runs-cli")).runWorkspaceRuns(ctx) },
1225
- { name: "workspace status", handler: async (ctx) => (await import("../workspace/status")).runWorkspaceStatus(ctx) },
1252
+ { name: "workspace status", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/status")).runWorkspaceStatus(ctx) },
1226
1253
  { name: "workspace lineage", handler: async (ctx) => (await import("../workspace/lineage-cli")).runWorkspaceLineage(ctx) },
1227
1254
  { name: "workspace upgrade", handler: async (ctx) => (await import("../workspace/lineage-upgrade-cli")).runWorkspaceUpgrade(ctx) },
1228
1255
  // #2551 — adopt a lineage, the hash index it matches against, and the versions of a family of workspaces.
@@ -85,7 +85,7 @@ describe("which servers have the workspace tools", () => {
85
85
  const { tools } = (await rpc(server(cwd), "tools/list")) as { tools: { name: string }[] };
86
86
  expect(tools.map((t) => t.name)).toEqual(expect.arrayContaining(names));
87
87
  }
88
- expect(names).toEqual(["workspace-ls", "workspace-status", "workspace-graph", "workspace-changes", "workspace-records", "workspace-points", "workspace-agent", "workspace-work-history", "workspace-runs", "records-new", "records-amend", "records-review", "records-close", "points-answer", "points-retract"]);
88
+ expect(names).toEqual(["workspace-ls", "workspace-status", "workspace-graph", "workspace-changes", "workspace-records", "workspace-points", "workspace-agent", "workspace-work-history", "workspace-runs", "records-new", "records-amend", "records-review", "records-close", "points-answer", "points-retract", "points-withdraw"]);
89
89
  });
90
90
 
91
91
  test("a server outside any workspace, or given none, does not", async () => {