@intentius/chant 0.100.0 → 0.101.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/dist/build.d.ts +6 -0
  2. package/dist/build.d.ts.map +1 -1
  3. package/dist/cli/build-options.d.ts +2 -0
  4. package/dist/cli/build-options.d.ts.map +1 -1
  5. package/dist/cli/commands/build.d.ts.map +1 -1
  6. package/dist/cli/commands/import.d.ts.map +1 -1
  7. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  8. package/dist/cli/main.d.ts.map +1 -1
  9. package/dist/cli/mcp/workspace-tools.d.ts +8 -0
  10. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  11. package/dist/cli/registry.d.ts +22 -0
  12. package/dist/cli/registry.d.ts.map +1 -1
  13. package/dist/config.d.ts +11 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/lexicon.d.ts +24 -1
  16. package/dist/lexicon.d.ts.map +1 -1
  17. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  18. package/dist/lifecycle/plan-digest.d.ts +26 -5
  19. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  20. package/dist/lint/config.d.ts +4 -4
  21. package/dist/op/activities/activity-contracts.d.ts +1 -0
  22. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  23. package/dist/op/activities/propose-upgrade.d.ts +2 -0
  24. package/dist/op/activities/propose-upgrade.d.ts.map +1 -1
  25. package/dist/op/index.d.ts +1 -1
  26. package/dist/op/index.d.ts.map +1 -1
  27. package/dist/serializer.d.ts +8 -0
  28. package/dist/serializer.d.ts.map +1 -1
  29. package/dist/telemetry-attribution.d.ts +77 -0
  30. package/dist/telemetry-attribution.d.ts.map +1 -0
  31. package/dist/workspace/agent-cli.d.ts +83 -0
  32. package/dist/workspace/agent-cli.d.ts.map +1 -0
  33. package/dist/workspace/changes-cli.d.ts.map +1 -1
  34. package/dist/workspace/changes.d.ts +8 -1
  35. package/dist/workspace/changes.d.ts.map +1 -1
  36. package/dist/workspace/checks/links.d.ts +1 -0
  37. package/dist/workspace/checks/links.d.ts.map +1 -1
  38. package/dist/workspace/checks/live.d.ts +40 -0
  39. package/dist/workspace/checks/live.d.ts.map +1 -0
  40. package/dist/workspace/checks.d.ts +21 -2
  41. package/dist/workspace/checks.d.ts.map +1 -1
  42. package/dist/workspace/compose-graph.d.ts +63 -0
  43. package/dist/workspace/compose-graph.d.ts.map +1 -1
  44. package/dist/workspace/decide.d.ts +1 -1
  45. package/dist/workspace/decide.d.ts.map +1 -1
  46. package/dist/workspace/declaration.d.ts +32 -0
  47. package/dist/workspace/declaration.d.ts.map +1 -1
  48. package/dist/workspace/declaration.schema.json +138 -3
  49. package/dist/workspace/export-cli.d.ts +12 -0
  50. package/dist/workspace/export-cli.d.ts.map +1 -0
  51. package/dist/workspace/export.d.ts +145 -0
  52. package/dist/workspace/export.d.ts.map +1 -0
  53. package/dist/workspace/graph-cli.d.ts.map +1 -1
  54. package/dist/workspace/import.d.ts +73 -0
  55. package/dist/workspace/import.d.ts.map +1 -0
  56. package/dist/workspace/kinds.d.ts +6 -2
  57. package/dist/workspace/kinds.d.ts.map +1 -1
  58. package/dist/workspace/lineage-adopt-cli.d.ts +15 -0
  59. package/dist/workspace/lineage-adopt-cli.d.ts.map +1 -0
  60. package/dist/workspace/lineage-adopt.d.ts +106 -0
  61. package/dist/workspace/lineage-adopt.d.ts.map +1 -0
  62. package/dist/workspace/lineage-check.d.ts +9 -2
  63. package/dist/workspace/lineage-check.d.ts.map +1 -1
  64. package/dist/workspace/lineage-cli.d.ts +6 -1
  65. package/dist/workspace/lineage-cli.d.ts.map +1 -1
  66. package/dist/workspace/lineage-hash-index.d.ts +110 -0
  67. package/dist/workspace/lineage-hash-index.d.ts.map +1 -0
  68. package/dist/workspace/lineage-init.d.ts +10 -0
  69. package/dist/workspace/lineage-init.d.ts.map +1 -1
  70. package/dist/workspace/lineage-lock.d.ts +147 -0
  71. package/dist/workspace/lineage-lock.d.ts.map +1 -1
  72. package/dist/workspace/lineage-migrations.d.ts +15 -3
  73. package/dist/workspace/lineage-migrations.d.ts.map +1 -1
  74. package/dist/workspace/lineage-provenance.d.ts +18 -0
  75. package/dist/workspace/lineage-provenance.d.ts.map +1 -0
  76. package/dist/workspace/lineage-upgrade-cli.d.ts +2 -0
  77. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -1
  78. package/dist/workspace/lineage-upgrade.d.ts +30 -1
  79. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  80. package/dist/workspace/lineage-versions.d.ts +116 -0
  81. package/dist/workspace/lineage-versions.d.ts.map +1 -0
  82. package/dist/workspace/links.d.ts +31 -5
  83. package/dist/workspace/links.d.ts.map +1 -1
  84. package/dist/workspace/ls-generated.d.ts +37 -0
  85. package/dist/workspace/ls-generated.d.ts.map +1 -0
  86. package/dist/workspace/ls.d.ts +4 -0
  87. package/dist/workspace/ls.d.ts.map +1 -1
  88. package/dist/workspace/member-commands.d.ts.map +1 -1
  89. package/dist/workspace/nested-graph.d.ts +56 -0
  90. package/dist/workspace/nested-graph.d.ts.map +1 -0
  91. package/dist/workspace/nesting.d.ts +21 -0
  92. package/dist/workspace/nesting.d.ts.map +1 -0
  93. package/dist/workspace/pin-cli.d.ts +10 -0
  94. package/dist/workspace/pin-cli.d.ts.map +1 -0
  95. package/dist/workspace/pin-integrity.d.ts +51 -0
  96. package/dist/workspace/pin-integrity.d.ts.map +1 -0
  97. package/dist/workspace/reason-codes.d.ts +26 -2
  98. package/dist/workspace/reason-codes.d.ts.map +1 -1
  99. package/dist/workspace/record-sessions.d.ts +7 -11
  100. package/dist/workspace/record-sessions.d.ts.map +1 -1
  101. package/dist/workspace/records-cli.d.ts +30 -1
  102. package/dist/workspace/records-cli.d.ts.map +1 -1
  103. package/dist/workspace/records-close.d.ts +5 -2
  104. package/dist/workspace/records-close.d.ts.map +1 -1
  105. package/dist/workspace/records-write.d.ts +22 -4
  106. package/dist/workspace/records-write.d.ts.map +1 -1
  107. package/dist/workspace/records.d.ts +43 -5
  108. package/dist/workspace/records.d.ts.map +1 -1
  109. package/dist/workspace/returns.d.ts +129 -0
  110. package/dist/workspace/returns.d.ts.map +1 -0
  111. package/dist/workspace/status-gates.d.ts.map +1 -1
  112. package/dist/workspace/template-manifest.d.ts +11 -3
  113. package/dist/workspace/template-manifest.d.ts.map +1 -1
  114. package/dist/workspace/trust/attestor.d.ts +8 -0
  115. package/dist/workspace/trust/attestor.d.ts.map +1 -1
  116. package/dist/workspace/trust/dsse.d.ts +58 -0
  117. package/dist/workspace/trust/dsse.d.ts.map +1 -0
  118. package/dist/workspace/trust/evidence-cli.d.ts +66 -0
  119. package/dist/workspace/trust/evidence-cli.d.ts.map +1 -0
  120. package/dist/workspace/trust/evidence.d.ts +93 -0
  121. package/dist/workspace/trust/evidence.d.ts.map +1 -0
  122. package/dist/workspace/trust/policy.d.ts +54 -1
  123. package/dist/workspace/trust/policy.d.ts.map +1 -1
  124. package/dist/workspace/trust/provenance.d.ts +21 -1
  125. package/dist/workspace/trust/provenance.d.ts.map +1 -1
  126. package/dist/workspace/trust/rotation.d.ts +132 -0
  127. package/dist/workspace/trust/rotation.d.ts.map +1 -0
  128. package/dist/workspace/trust/seal.d.ts.map +1 -1
  129. package/dist/workspace/trust/signers-cli.d.ts +49 -0
  130. package/dist/workspace/trust/signers-cli.d.ts.map +1 -0
  131. package/dist/workspace/trust/ssh-commit.d.ts +12 -0
  132. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  133. package/dist/workspace/trust/test-repo.d.ts +13 -0
  134. package/dist/workspace/trust/test-repo.d.ts.map +1 -1
  135. package/dist/workspace/trust/verify.d.ts +15 -0
  136. package/dist/workspace/trust/verify.d.ts.map +1 -1
  137. package/dist/workspace/work-evidence.d.ts +1 -1
  138. package/dist/workspace/work-evidence.d.ts.map +1 -1
  139. package/dist/workspace/write-scope.d.ts +199 -0
  140. package/dist/workspace/write-scope.d.ts.map +1 -0
  141. package/package.json +1 -1
  142. package/src/build.ts +8 -0
  143. package/src/cli/build-options.ts +4 -1
  144. package/src/cli/commands/build.ts +2 -0
  145. package/src/cli/commands/import-live.test.ts +69 -1
  146. package/src/cli/commands/import.ts +48 -22
  147. package/src/cli/handlers/fan-out.test.ts +6 -6
  148. package/src/cli/handlers/fan-out.ts +2 -1
  149. package/src/cli/handlers/graph.test.ts +42 -0
  150. package/src/cli/handlers/graph.ts +22 -0
  151. package/src/cli/handlers/operator.ts +1 -1
  152. package/src/cli/main.test.ts +32 -0
  153. package/src/cli/main.ts +103 -6
  154. package/src/cli/mcp/workspace-tools.test.ts +1 -1
  155. package/src/cli/mcp/workspace-tools.ts +33 -2
  156. package/src/cli/registry.ts +22 -0
  157. package/src/cli/serve-mcp-workspace.test.ts +1 -1
  158. package/src/codegen/release-wiring.test.ts +5 -1
  159. package/src/components/fan-out-output.test.ts +1 -1
  160. package/src/components/fan-out.test.ts +1 -1
  161. package/src/components/promote.test.ts +1 -1
  162. package/src/config.ts +12 -0
  163. package/src/content-digest.test.ts +2 -2
  164. package/src/lexicon.ts +25 -1
  165. package/src/lifecycle/gate-ledger.test.ts +14 -0
  166. package/src/lifecycle/gate-ledger.ts +2 -1
  167. package/src/lifecycle/plan-digest.test.ts +54 -3
  168. package/src/lifecycle/plan-digest.ts +38 -8
  169. package/src/op/activities/activity-contracts.ts +1 -0
  170. package/src/op/activities/propose-upgrade.ts +8 -5
  171. package/src/op/gate-approval.test.ts +17 -0
  172. package/src/op/gate.ts +3 -3
  173. package/src/op/index.ts +1 -1
  174. package/src/serializer.ts +9 -0
  175. package/src/telemetry-attribution.test.ts +91 -0
  176. package/src/telemetry-attribution.ts +145 -0
  177. package/src/workspace/agent-cli.ts +134 -0
  178. package/src/workspace/agent.schema.json +356 -0
  179. package/src/workspace/behold-kinds.test.ts +1 -1
  180. package/src/workspace/changes-cli.ts +5 -0
  181. package/src/workspace/changes.schema.json +179 -1
  182. package/src/workspace/changes.ts +58 -4
  183. package/src/workspace/check-live.test.ts +192 -0
  184. package/src/workspace/check.schema.json +64 -0
  185. package/src/workspace/checks/links.ts +23 -2
  186. package/src/workspace/checks/live.ts +113 -0
  187. package/src/workspace/checks.test.ts +2 -2
  188. package/src/workspace/checks.ts +20 -3
  189. package/src/workspace/compose-graph.test.ts +47 -0
  190. package/src/workspace/compose-graph.ts +120 -3
  191. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +7 -1
  192. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +5 -0
  193. package/src/workspace/declaration.schema.json +138 -3
  194. package/src/workspace/declaration.ts +106 -1
  195. package/src/workspace/declared-kinds.test.ts +33 -0
  196. package/src/workspace/evidence.schema.json +279 -0
  197. package/src/workspace/export-cli.ts +180 -0
  198. package/src/workspace/export-import.test.ts +282 -0
  199. package/src/workspace/export.ts +486 -0
  200. package/src/workspace/graph-cli.ts +47 -1
  201. package/src/workspace/graph-contract.test.ts +89 -4
  202. package/src/workspace/graph.schema.json +206 -1
  203. package/src/workspace/import.ts +325 -0
  204. package/src/workspace/kinds.test.ts +5 -5
  205. package/src/workspace/kinds.ts +25 -3
  206. package/src/workspace/lineage-adopt-cli.ts +103 -0
  207. package/src/workspace/lineage-adopt.test.ts +552 -0
  208. package/src/workspace/lineage-adopt.ts +452 -0
  209. package/src/workspace/lineage-check.ts +26 -5
  210. package/src/workspace/lineage-cli.ts +12 -2
  211. package/src/workspace/lineage-hash-index.ts +305 -0
  212. package/src/workspace/lineage-init.test.ts +9 -0
  213. package/src/workspace/lineage-init.ts +29 -9
  214. package/src/workspace/lineage-lock.ts +54 -0
  215. package/src/workspace/lineage-migrations.test.ts +27 -0
  216. package/src/workspace/lineage-migrations.ts +44 -14
  217. package/src/workspace/lineage-provenance.ts +40 -0
  218. package/src/workspace/lineage-upgrade-cli.ts +9 -5
  219. package/src/workspace/lineage-upgrade.test.ts +92 -1
  220. package/src/workspace/lineage-upgrade.ts +111 -16
  221. package/src/workspace/lineage-versions.ts +348 -0
  222. package/src/workspace/links.test.ts +121 -2
  223. package/src/workspace/links.ts +97 -6
  224. package/src/workspace/ls-contract.test.ts +84 -1
  225. package/src/workspace/ls-generated.ts +111 -0
  226. package/src/workspace/ls.schema.json +19 -0
  227. package/src/workspace/ls.ts +8 -1
  228. package/src/workspace/member-commands.ts +3 -1
  229. package/src/workspace/nested-graph.test.ts +176 -0
  230. package/src/workspace/nested-graph.ts +169 -0
  231. package/src/workspace/nesting.ts +37 -0
  232. package/src/workspace/pin-cli.test.ts +71 -0
  233. package/src/workspace/pin-cli.ts +57 -0
  234. package/src/workspace/pin-integrity.test.ts +121 -0
  235. package/src/workspace/pin-integrity.ts +104 -0
  236. package/src/workspace/points-write.schema.json +1 -0
  237. package/src/workspace/read-contract.test.ts +24 -0
  238. package/src/workspace/reason-codes.test.ts +10 -0
  239. package/src/workspace/reason-codes.ts +29 -2
  240. package/src/workspace/record-sessions.ts +12 -14
  241. package/src/workspace/records-amend.schema.json +4 -0
  242. package/src/workspace/records-cli.ts +93 -12
  243. package/src/workspace/records-close.schema.json +6 -2
  244. package/src/workspace/records-close.ts +10 -2
  245. package/src/workspace/records-formats.test.ts +13 -5
  246. package/src/workspace/records-new.schema.json +4 -0
  247. package/src/workspace/records-review.schema.json +4 -0
  248. package/src/workspace/records-sessions.test.ts +23 -6
  249. package/src/workspace/records-write.test.ts +65 -8
  250. package/src/workspace/records-write.ts +70 -6
  251. package/src/workspace/records.schema.json +55 -3
  252. package/src/workspace/records.ts +96 -5
  253. package/src/workspace/returns.ts +328 -0
  254. package/src/workspace/signers.schema.json +206 -0
  255. package/src/workspace/status-gates.ts +2 -1
  256. package/src/workspace/template-manifest.ts +22 -4
  257. package/src/workspace/trust/attestor.ts +15 -0
  258. package/src/workspace/trust/dsse.ts +134 -0
  259. package/src/workspace/trust/evidence-cli.ts +195 -0
  260. package/src/workspace/trust/evidence.test.ts +241 -0
  261. package/src/workspace/trust/evidence.ts +207 -0
  262. package/src/workspace/trust/policy.ts +110 -3
  263. package/src/workspace/trust/provenance.ts +41 -4
  264. package/src/workspace/trust/record-seal.test.ts +1 -1
  265. package/src/workspace/trust/rotation.test.ts +258 -0
  266. package/src/workspace/trust/rotation.ts +336 -0
  267. package/src/workspace/trust/seal.ts +11 -0
  268. package/src/workspace/trust/signers-cli.ts +178 -0
  269. package/src/workspace/trust/ssh-commit.ts +53 -4
  270. package/src/workspace/trust/test-repo.ts +18 -0
  271. package/src/workspace/trust/trust.test.ts +8 -2
  272. package/src/workspace/trust/verify-cli.ts +1 -0
  273. package/src/workspace/trust/verify.ts +22 -0
  274. package/src/workspace/work-evidence.schema.json +4 -0
  275. package/src/workspace/write-scope.test.ts +340 -0
  276. package/src/workspace/write-scope.ts +448 -0
@@ -204,8 +204,8 @@ describe("proof: a plan-only invocation prints the derivation and dispatches not
204
204
  expect(out).toContain("wave 1: cluster-a");
205
205
  expect(out).toContain("seeded from an earlier run: net");
206
206
  expect(out).toMatch(/billing +unaffected/);
207
- expect(out).toMatch(/plan: sha256:[0-9a-f]{64}/);
208
- expect(out).toMatch(/approve: chant approve fan-out release --plan sha256:[0-9a-f]{64}/);
207
+ expect(out).toMatch(/plan: jcs1-sha256:[0-9a-f]{64}/);
208
+ expect(out).toMatch(/approve: chant approve fan-out release --plan jcs1-sha256:[0-9a-f]{64}/);
209
209
  });
210
210
 
211
211
  test("--dry-run with --resume shows what is left, at the digest that approves it", async () => {
@@ -214,7 +214,7 @@ describe("proof: a plan-only invocation prints the derivation and dispatches not
214
214
  const resume = join(dir, "attempt.json");
215
215
  failing = ["cluster-a"];
216
216
  await runComponentsFanOut(ctx({ base: "main", env: "test", resume }));
217
- const digest = /plan: (sha256:[0-9a-f]{64})/.exec(stderr())?.[1];
217
+ const digest = /plan: (jcs1-sha256:[0-9a-f]{64})/.exec(stderr())?.[1];
218
218
 
219
219
  ran = [];
220
220
  stderrLines = [];
@@ -236,7 +236,7 @@ describe("proof: a plan-only invocation prints the derivation and dispatches not
236
236
  expect(exit).toBe(0);
237
237
  const parsed = JSON.parse(stdout());
238
238
  expect(parsed.order).toEqual(["net", "cluster-a", "cluster-b", "app-one", "app-three", "app-two"]);
239
- expect(parsed.digest).toMatch(/^sha256:[0-9a-f]{64}$/);
239
+ expect(parsed.digest).toMatch(/^jcs1-sha256:[0-9a-f]{64}$/);
240
240
  });
241
241
  });
242
242
 
@@ -334,10 +334,10 @@ describe("proof: one approval covers the set, and the printed digest is what app
334
334
  expect(ran).toEqual([]);
335
335
  const out = stderr();
336
336
  expect(out).toContain("gated: nothing ran.");
337
- expect(out).toMatch(/approve : chant approve fan-out release --plan sha256:[0-9a-f]{64}/);
337
+ expect(out).toMatch(/approve : chant approve fan-out release --plan jcs1-sha256:[0-9a-f]{64}/);
338
338
  // One pending fact for the whole set, not one per component.
339
339
  expect(gateLedger.appended).toHaveLength(1);
340
- expect(gateLedger.appended[0].planDigest).toBe(/plan: (sha256:[0-9a-f]{64})/.exec(out)?.[1]);
340
+ expect(gateLedger.appended[0].planDigest).toBe(/plan: (jcs1-sha256:[0-9a-f]{64})/.exec(out)?.[1]);
341
341
  });
342
342
 
343
343
  test("the resolution for that digest lets the whole set through", async () => {
@@ -38,6 +38,7 @@
38
38
  import { resolve, dirname } from "node:path";
39
39
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
40
40
  import { loadChantConfig, resolveAutoReleaseDisabled, type ChantConfig } from "../../config";
41
+ import { samePlanDigest } from "../../lifecycle/plan-digest";
41
42
  import { affectedStacks } from "../../lifecycle/affected";
42
43
  import { deriveFanOut, fanOutRegistry } from "../../components/fan-out-support";
43
44
  import { runFanOut } from "../../components/fan-out-run";
@@ -242,7 +243,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
242
243
  console.error(formatError({ message: `--resume: could not read "${args.resume}": ${err instanceof Error ? err.message : String(err)}` }));
243
244
  return 1;
244
245
  }
245
- if (attempt && attempt.digest !== derived.plan.digest) {
246
+ if (attempt && !samePlanDigest(attempt.digest, derived.plan.digest)) {
246
247
  console.error(formatWarning({
247
248
  message: `--resume: "${args.resume}" records a different fan-out (${attempt.digest || "no digest"}), so its progress does not apply here`,
248
249
  hint: "The derivation changed, which makes this a different change. Everything selected will run.",
@@ -161,6 +161,7 @@ describe("runGraph", () => {
161
161
  observeMock.mockReset();
162
162
  buildMock.mockReset();
163
163
  loadPluginsMock.mockReset();
164
+ loadPluginsMock.mockResolvedValue([]);
164
165
  resolveLexMock.mockReset();
165
166
  loadChantConfigMock.mockReset();
166
167
  loadChantConfigMock.mockResolvedValue({ config: {} });
@@ -228,6 +229,47 @@ describe("runGraph", () => {
228
229
  expect(ir.edges).toContainEqual({ from: "subnet", to: "vpc", kind: "ref", viaAttr: "network" });
229
230
  });
230
231
 
232
+ // #2559 — a lexicon's graphMeta answer lands in the IR meta, so the
233
+ // workspace graph can list collector pipelines without core importing otel.
234
+ test("--format ir carries what a lexicon's graphMeta reports under meta", async () => {
235
+ lintClean(); discovered();
236
+ const seen: string[][] = [];
237
+ loadPluginsMock.mockResolvedValue([{
238
+ name: "gcp",
239
+ serializer: {},
240
+ graphMeta: (entities: Map<string, unknown>) => {
241
+ seen.push([...entities.keys()].sort());
242
+ return { collector: { pipelines: [] } };
243
+ },
244
+ }]);
245
+ resolveLexMock.mockResolvedValue(["gcp"]);
246
+ const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
247
+ expect(exit).toBe(0);
248
+ expect(seen).toEqual([["pod", "subnet", "vpc"]]);
249
+ expect(JSON.parse(stdoutBuf.join("\n")).meta).toEqual({ collector: { pipelines: [] } });
250
+ });
251
+
252
+ test("--format ir has no meta when no lexicon reports anything", async () => {
253
+ lintClean(); discovered();
254
+ loadPluginsMock.mockResolvedValue([{ name: "gcp", serializer: {}, graphMeta: () => undefined }, { name: "k8s", serializer: {} }]);
255
+ resolveLexMock.mockResolvedValue(["gcp", "k8s"]);
256
+ const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
257
+ expect(exit).toBe(0);
258
+ expect("meta" in JSON.parse(stdoutBuf.join("\n"))).toBe(false);
259
+ });
260
+
261
+ test("--format ir keeps the first answer when two lexicons report one key", async () => {
262
+ lintClean(); discovered();
263
+ loadPluginsMock.mockResolvedValue([
264
+ { name: "a", serializer: {}, graphMeta: () => ({ collector: 1 }) },
265
+ { name: "b", serializer: {}, graphMeta: () => ({ collector: 2 }) },
266
+ ]);
267
+ resolveLexMock.mockResolvedValue(["a", "b"]);
268
+ const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
269
+ expect(exit).toBe(0);
270
+ expect(JSON.parse(stdoutBuf.join("\n")).meta).toEqual({ collector: 1 });
271
+ });
272
+
231
273
  // #2529 — the IR carries its version, as the first key, so a reader can
232
274
  // decide how to read the rest before it reads anything else.
233
275
  test("--format ir leads with the IR version", async () => {
@@ -702,6 +702,27 @@ async function buildPipelineProjection(
702
702
  * Lint-gated: the IR represents valid infra, so we refuse to emit for source that
703
703
  * does not pass lint. Non-zero on discovery errors or a layout-engine failure.
704
704
  */
705
+ /**
706
+ * Add what the lexicons' `graphMeta` hooks report to `ir.meta` (#2559). A
707
+ * lexicon with nothing to report leaves the IR as it was, so a project that
708
+ * declares none of their entities prints what it did before. Two lexicons
709
+ * that answer the same key keep the first and say so on stderr.
710
+ */
711
+ function withGraphMeta(ir: GraphIR, plugins: LexiconPlugin[], entities: Map<string, import("../../declarable").Declarable>): GraphIR {
712
+ const added: Record<string, unknown> = {};
713
+ for (const p of plugins) {
714
+ if (!p.graphMeta) continue;
715
+ for (const [key, value] of Object.entries(p.graphMeta(entities) ?? {})) {
716
+ if (key in added || (ir.meta && key in ir.meta)) {
717
+ console.error(formatWarning({ message: `the ${p.name} lexicon reports graph meta "${key}", which another source already set; its value is dropped` }));
718
+ continue;
719
+ }
720
+ added[key] = value;
721
+ }
722
+ }
723
+ return Object.keys(added).length === 0 ? ir : { ...ir, meta: { ...(ir.meta ?? {}), ...added } };
724
+ }
725
+
705
726
  /** Returned instead of a graph when the prediction cannot be attempted honestly. */
706
727
  const REFUSED = Symbol("behaviour-refused");
707
728
 
@@ -855,6 +876,7 @@ async function runGraphView(
855
876
  });
856
877
  if (predicted === REFUSED) return 1;
857
878
  ir = predicted;
879
+ ir = withGraphMeta(ir, plugins as LexiconPlugin[], result.entities);
858
880
  }
859
881
 
860
882
  if (ctx.args.lens) {
@@ -1047,7 +1047,7 @@ export async function recordGateApproval(
1047
1047
  if (opts.plan !== undefined) {
1048
1048
  if (!isPlanDigest(opts.plan)) {
1049
1049
  console.error(formatError({
1050
- message: `--plan must be a plan digest ("sha256:" and 64 hex characters), got "${opts.plan}"`,
1050
+ message: `--plan must be a plan digest ("jcs1-sha256:" or "sha256:", then 64 hex characters), got "${opts.plan}"`,
1051
1051
  hint: "Copy it from the gated run's `plan :` line, or from the pending-gate summary. It is not a plan file path.",
1052
1052
  }));
1053
1053
  return { ok: false };
@@ -747,25 +747,57 @@ describe("workspace init and ls (#2534)", () => {
747
747
  expect(resolveCommand(init, commandRegistry)?.def.name).toBe("workspace init");
748
748
  expect(commandRegistry.filter((c) => c.name === "workspace" || c.name.startsWith("workspace ")).map((c) => c.name).sort()).toEqual([
749
749
  "workspace",
750
+ "workspace admit",
751
+ "workspace adopt-lineage",
752
+ "workspace agent",
750
753
  "workspace audit",
751
754
  "workspace build",
752
755
  "workspace check",
756
+ "workspace evidence",
757
+ "workspace export",
753
758
  "workspace graph",
759
+ "workspace hash-index",
760
+ "workspace import",
754
761
  "workspace init",
755
762
  "workspace lineage",
756
763
  "workspace lint",
757
764
  "workspace ls",
758
765
  "workspace member-run",
759
766
  "workspace patch",
767
+ "workspace pin",
760
768
  "workspace points",
761
769
  "workspace records",
770
+ "workspace signers",
762
771
  "workspace status",
763
772
  "workspace upgrade",
764
773
  "workspace verify",
774
+ "workspace versions",
765
775
  "workspace work",
766
776
  ]);
767
777
  });
768
778
 
779
+ test("adopt-lineage, hash-index, versions and upgrade --source take their flags (#2551)", () => {
780
+ const adopt = parseArgs(["workspace", "adopt-lineage", "app", "--from", "acme/starter#svc", "--tags", "v*", "--index", "i.json", "--param", "name=x", "--dry-run"]);
781
+ expect(adopt).toMatchObject({ command: "workspace", path: "adopt-lineage", extraPositional: "app", migrateFrom: "acme/starter#svc", tags: "v*", index: "i.json", param: ["name=x"], dryRun: true });
782
+ expect(resolveCommand(adopt, commandRegistry)?.def.name).toBe("workspace adopt-lineage");
783
+ expect(parseArgs(["workspace", "versions", "--available", "--template", "x"])).toMatchObject({ path: "versions", available: true, template: "x" });
784
+ expect(parseArgs(["workspace", "upgrade", "--source", "acme/upstream", "--to", "v2.0.0"])).toMatchObject({ source: "acme/upstream", migrateTo: "v2.0.0" });
785
+ expect(() => parseArgs(["workspace", "adopt-lineage", "--tags"])).toThrow(/--tags needs a tag glob/);
786
+ expect(() => parseArgs(["workspace", "upgrade", "--source", "--to"])).toThrow(/--source needs a template/);
787
+ });
788
+
789
+ test("export, import and admit take their arguments (#2552)", () => {
790
+ const exp = parseArgs(["workspace", "export", "app,design", "--to", "out", "--param", "domain=example.com", "--dry-run", "--json"]);
791
+ expect(exp).toMatchObject({ command: "workspace", path: "export", extraPositional: "app,design", migrateTo: "out", param: ["domain=example.com"], dryRun: true, json: true });
792
+ expect(resolveCommand(exp, commandRegistry)?.def.name).toBe("workspace export");
793
+ const imp = parseArgs(["workspace", "import", "../copy", "--remove"]);
794
+ expect(imp).toMatchObject({ path: "import", extraPositional: "../copy", remove: true });
795
+ expect(resolveCommand(imp, commandRegistry)?.def.name).toBe("workspace import");
796
+ const admit = parseArgs(["workspace", "admit", "ret-0123456789ab", "--note", "returned from the studio"]);
797
+ expect(admit).toMatchObject({ path: "admit", extraPositional: "ret-0123456789ab", note: "returned from the studio" });
798
+ expect(resolveCommand(admit, commandRegistry)?.def.name).toBe("workspace admit");
799
+ });
800
+
769
801
  test("workspace work takes its verb, id and lease flags (#2732)", () => {
770
802
  const claim = parseArgs(["workspace", "work", "claim", "W-001", "--holder", "a", "--ttl", "60", "--json"]);
771
803
  expect(claim).toMatchObject({ command: "workspace", path: "work", extraPositional: "claim", extraPositional2: "W-001", holder: "a", ttl: "60", json: true });
package/src/cli/main.ts CHANGED
@@ -456,6 +456,12 @@ export function parseArgs(args: string[]): ParsedArgs {
456
456
  // `chant workspace records new <kind> --prefix <prefix>` (#2670): the id prefix to allocate under.
457
457
  result.prefix = args[++i];
458
458
  if (!result.prefix || result.prefix.startsWith("-")) throw new Error("--prefix needs an id prefix: --prefix <prefix>");
459
+ } else if (arg === "--key" || arg === "--check-id" || arg === "--claim" || arg === "--environment" || arg === "--envelope" || arg === "--threshold") {
460
+ // `chant workspace evidence sign|verify` and `chant workspace signers` (#2553)
461
+ const value = args[++i];
462
+ if (value === undefined || value.startsWith("-")) throw new Error(`${arg} needs a value`);
463
+ const field = ({ "--key": "key", "--check-id": "checkId", "--claim": "claim", "--environment": "environment", "--envelope": "envelope", "--threshold": "threshold" } as const)[arg];
464
+ result[field] = value;
459
465
  } else if (arg === "--require") {
460
466
  // `chant workspace records|verify --require attested` (#2547)
461
467
  result.require = args[++i];
@@ -576,6 +582,24 @@ export function parseArgs(args: string[]): ParsedArgs {
576
582
  // #2550 — `chant workspace upgrade` runs a template's code migrations
577
583
  // only when asked to.
578
584
  result.allowCode = true;
585
+ } else if (arg === "--source") {
586
+ // #2551 — `chant workspace upgrade --source <repo>[#<member>]`.
587
+ result.source = args[++i];
588
+ if (!result.source || result.source.startsWith("-")) throw new Error("--source needs a template: --source <repo>[#<member>]");
589
+ } else if (arg === "--tags") {
590
+ // #2551 — `chant workspace adopt-lineage --tags <glob>`, also hash-index and versions.
591
+ result.tags = args[++i];
592
+ if (!result.tags || result.tags.startsWith("-")) throw new Error("--tags needs a tag glob: --tags 'v*'");
593
+ } else if (arg === "--index") {
594
+ // #2551 — `chant workspace adopt-lineage --index <file>`.
595
+ result.index = args[++i];
596
+ if (!result.index || result.index.startsWith("-")) throw new Error("--index needs a hash index file: --index <file>");
597
+ } else if (arg === "--remove") {
598
+ // #2552 — `chant workspace import --remove` removes the export member instead of writing it again.
599
+ result.remove = true;
600
+ } else if (arg === "--available") {
601
+ // #2551 — `chant workspace versions --available` lists the template's version tags.
602
+ result.available = true;
579
603
  } else if (arg === "--root-only") {
580
604
  // #2537 — `chant build` and `chant lint` at a declared workspace root
581
605
  // run on the root project alone instead of refusing with WSP000.
@@ -909,24 +933,77 @@ Workspace (level 1, #2524):
909
933
  workspace points answer <id> --answer <value> --by <name>... [--kind <kind file>] [--dry-run]
910
934
  Record people's answer to an open question, or confirm
911
935
  a model's proposal, once the point's quorum is met
936
+ workspace pin <path> [--json]
937
+ Print the integrity value that pins the plugin at
938
+ <path>, to put in a path pin of chant.workspace.json.
939
+ chant checks it before it reads the plugin
940
+ workspace agent [<name>] [--json]
941
+ Print an agent session from the declaration: the one
942
+ member it is bound to, the record kinds and verbs its
943
+ write scope allows, and the spec records --current
944
+ prints. Without a name, the session CHANT_AGENT names
912
945
  workspace verify [--base <rev>] [--head <rev>] [--require attested]
913
946
  Check the commits in base..head against the signers
914
947
  and roles read from base. A change to the signers file
915
948
  or .chant/trust.json needs a signature by a signer
916
- trusted at base. Does nothing without a signers file
949
+ trusted at base, and a new signer set needs a
950
+ threshold of the old one. Does nothing without a
951
+ signers file
952
+ workspace signers [--json] [rotate [--threshold <n>] | sign --key <file>]
953
+ Show the signer history at base, or propose the next
954
+ signer set and sign it with a current signer's key
955
+ workspace evidence sign --kind <kind file> --key <runner.pem> --check-id <id>
956
+ Sign runner evidence over the records' hashes in a
957
+ DSSE envelope, with a runner key .chant/trust.json
958
+ lists. For CI and services, never a person's key
959
+ workspace evidence verify --envelope <file> [--at <rev>] [--json]
960
+ Verify runner evidence offline, and whether the
961
+ records it covers are unchanged
917
962
  workspace lineage [--json]
918
963
  Show each scope in .chant/workspace.lock.json: its
919
964
  template and pin, locally edited files and open
920
965
  manual steps. Needs no workspace file
921
966
  workspace lineage resolve <path>
922
967
  Close a manual step once the file is merged by hand
923
- workspace upgrade [<scope>] [--to <ref|dir>] [--allow-code] [--dry-run] [--output <file>]
968
+ workspace upgrade [<scope>] [--to <ref|dir>] [--source <repo>[#<member>]] [--allow-code] [--dry-run] [--output <file>]
924
969
  Bring a lineage scope to a newer template version: fetch
925
970
  it, migrate and merge per file in a worktree, run build,
926
971
  lint and workspace check there, then gate on the digest
927
972
  of the patch (chant approve workspace-upgrade <scope>).
928
- A second run with the approval applies the patch
929
- workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated] [--kind <kind file>]
973
+ A second run with the approval applies the patch.
974
+ --source <repo>[#<member>] moves the scope to another
975
+ template through that template's bridge migration
976
+ workspace adopt-lineage [<scope>] --from <repo>[@<ref>][#<member>] [--tags <glob>]
977
+ [--index <file>] [--param name=value] [--dry-run] [--json]
978
+ Give a scope a git lineage: match a scope with none
979
+ against the template's versions and record the best
980
+ one, with the adopted commit range in .chant/trust.json,
981
+ or move a directory lineage onto the version that
982
+ reproduces its recorded files
983
+ workspace hash-index --from <repo>[#<member>] [--tags <glob>] [--output <file>]
984
+ Compute the per-version file hashes adopt-lineage
985
+ matches against, for a template's CI to publish
986
+ workspace versions [<dir>] [--template <id>] [--available [--tags <glob>]] [--json]
987
+ Report the template, chant and lexicon versions of
988
+ every lineage lock under <dir>, grouped into families
989
+ by template; --available also lists each git
990
+ template's version tags and where each lock sits
991
+ workspace export [<member>[,<member>...]] [--to <member>] [--param name=value] [--dry-run] [--json]
992
+ Write members that set travel, with their lineage and
993
+ records, into the export member (role export) as a
994
+ workspace of their own; with no member named, every
995
+ member that travels and the workspace's own records.
996
+ --param gives a host-bound parameter its value there
997
+ workspace import [<dir>] [--remove] [--dry-run] [--json]
998
+ Bring an export back: merge per file against the
999
+ export's hashes, switch host values back, record the
1000
+ return in .chant/returns/<id>.json with the commits the
1001
+ files were made in, then write the export member again
1002
+ or, with --remove, remove it
1003
+ workspace admit <return id> [--note <text>] [--dry-run] [--json]
1004
+ Admit the signers of a return in .chant/trust.json, so
1005
+ its signed work reads as attested once merged
1006
+ workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated] [--kind <kind file>] [--live --env <env>]
930
1007
  Fail on an unreadable lineage lock or an open manual
931
1008
  step, and, in a declared workspace, on a WSP check of
932
1009
  the declaration, member ledgers, pipelines or
@@ -934,14 +1011,21 @@ Workspace (level 1, #2524):
934
1011
  and compares their output. --kind warns on records
935
1012
  whose pinned files changed. Needs no workspace file.
936
1013
  --at reads a commit's git objects; --format json
937
- prints the read-contract document
1014
+ prints the read-contract document. --live --env
1015
+ resolves each declared member link against the live
1016
+ graph of <env> (each chant member runs chant graph
1017
+ --live, which reaches the account) and reports
1018
+ WSP141 and WSP142
938
1019
  workspace check --changes <base>..<head> [--work <id>] [--severity off|warn|fail] [--kind <kind file>...] [--json]
939
1020
  Map each path the diff changes to the current records
940
1021
  whose constrains cover it: change-uncovered when none
941
1022
  does, change-out-of-scope when a record in hand (the
942
1023
  --work item and its decisions) lists it in
943
1024
  out_of_scope. The declaration's changes block sets
944
- the severity (warn by default) and ignore globs
1025
+ the severity (warn by default) and ignore globs. When
1026
+ the declaration at base has writeScope or agents, each
1027
+ commit that writes outside its writer's scope fails
1028
+ the check (write-scope-member, write-scope-kind)
945
1029
  workspace patch <base>..<head>|<base>...<head>|<commit> [--path <p>...] [--max-bytes <n>] [--json]
946
1030
  workspace patch [<commit>] --worktree [--path <p>...] [--max-bytes <n>] [--json]
947
1031
  The hunks of a diff, file by file: a range, a work
@@ -1471,6 +1555,14 @@ export const commandRegistry: CommandDef[] = [
1471
1555
  { name: "workspace status", handler: async (ctx) => (await import("../workspace/status")).runWorkspaceStatus(ctx) },
1472
1556
  { name: "workspace lineage", handler: async (ctx) => (await import("../workspace/lineage-cli")).runWorkspaceLineage(ctx) },
1473
1557
  { name: "workspace upgrade", handler: async (ctx) => (await import("../workspace/lineage-upgrade-cli")).runWorkspaceUpgrade(ctx) },
1558
+ // #2551 — adopt a lineage, the hash index it matches against, and the versions of a family of workspaces.
1559
+ { name: "workspace adopt-lineage", handler: async (ctx) => (await import("../workspace/lineage-adopt-cli")).runWorkspaceAdoptLineage(ctx) },
1560
+ { name: "workspace hash-index", handler: async (ctx) => (await import("../workspace/lineage-adopt-cli")).runWorkspaceHashIndex(ctx) },
1561
+ // #2552 — export a workspace or members, bring an export back, and admit a return's signers.
1562
+ { name: "workspace export", handler: async (ctx) => (await import("../workspace/export-cli")).runWorkspaceExport(ctx) },
1563
+ { name: "workspace import", handler: async (ctx) => (await import("../workspace/export-cli")).runWorkspaceImport(ctx) },
1564
+ { name: "workspace admit", handler: async (ctx) => (await import("../workspace/export-cli")).runWorkspaceAdmit(ctx) },
1565
+ { name: "workspace versions", handler: async (ctx) => (await import("../workspace/lineage-versions")).runWorkspaceVersions(ctx) },
1474
1566
  // #2641 — workspace check reads member configs statically and never runs one.
1475
1567
  { name: "workspace check", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/lineage-check")).runWorkspaceCheck(ctx) },
1476
1568
  // The hunks of a diff, for a reader that runs no git of its own.
@@ -1484,7 +1576,12 @@ export const commandRegistry: CommandDef[] = [
1484
1576
  { name: "workspace graph", handler: async (ctx) => (await import("../workspace/member-commands")).runWorkspaceMembers(ctx, "graph") },
1485
1577
  // Each unit decides for itself whether its config is loaded (runCommandInProcess).
1486
1578
  { name: "workspace member-run", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/member-run")).runWorkspaceMemberRun(ctx, runCommandInProcess) },
1579
+ { name: "workspace pin", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/pin-cli")).runWorkspacePin(ctx) },
1580
+ // #2548 — an agent session's member, write scope and spec, for a session that resumes.
1581
+ { name: "workspace agent", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/agent-cli")).runWorkspaceAgent(ctx) },
1487
1582
  { name: "workspace verify", handler: async (ctx) => (await import("../workspace/trust/verify-cli")).runWorkspaceVerify(ctx) },
1583
+ { name: "workspace signers", handler: async (ctx) => (await import("../workspace/trust/signers-cli")).runWorkspaceSigners(ctx) },
1584
+ { name: "workspace evidence", handler: async (ctx) => (await import("../workspace/trust/evidence-cli")).runWorkspaceEvidence(ctx) },
1488
1585
 
1489
1586
  // State subcommands
1490
1587
  { name: "lifecycle snapshot", requiresPlugins: true, handler: runLifecycleSnapshot },
@@ -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-work-history", "records-new", "records-amend", "records-review", "records-close", "points-answer"]);
88
+ expect(names).toEqual(["workspace-ls", "workspace-status", "workspace-graph", "workspace-changes", "workspace-records", "workspace-points", "workspace-agent", "workspace-work-history", "records-new", "records-amend", "records-review", "records-close", "points-answer"]);
89
89
  });
90
90
 
91
91
  test("a server outside any workspace, or given none, does not", async () => {
@@ -28,11 +28,20 @@
28
28
  *
29
29
  * Nothing here listens on a port or authenticates anyone (ws-052): the server
30
30
  * speaks over stdio, and `by` is recorded as given, as `--by` is.
31
+ *
32
+ * An agent session (#2548): a server started with `CHANT_AGENT` set writes in
33
+ * that session, bound to its one member, and its writes are refused outside
34
+ * the session's write scope with the CLI's codes (`write-scope-member`,
35
+ * `write-scope-kind`, `agent-unknown`). `workspace-agent` returns what the
36
+ * session reloads from.
31
37
  */
32
38
 
33
39
  import { spawn } from "node:child_process";
34
40
  import type { ToolContext, ToolDefinition, ToolHandler } from "./types";
35
41
 
42
+ /** `AGENT_ENV` of `workspace/write-scope.ts`, kept here so the server loads nothing under workspace/ until a tool is called. */
43
+ const AGENT_ENV = "CHANT_AGENT";
44
+
36
45
  /** How the reads run chant: a command and its leading arguments. */
37
46
  export type ChantCommand = string[];
38
47
 
@@ -49,6 +58,8 @@ export interface WorkspaceToolsOptions {
49
58
  cwd: string;
50
59
  /** The chant the reads run. Defaults to {@link ownChantCommand}. */
51
60
  chantCommand?: ChantCommand;
61
+ /** The agent session the writes are made in (#2548). Defaults to `CHANT_AGENT`. */
62
+ agent?: string;
52
63
  }
53
64
 
54
65
  const PROTOCOL =
@@ -154,6 +165,17 @@ export const workspaceReadTools: ToolDefinition[] = [
154
165
  },
155
166
  },
156
167
  },
168
+ {
169
+ name: "workspace-agent",
170
+ description:
171
+ "An agent session and what it reloads from (#2548): chant workspace agent <name> --json (agent.schema.json). The one member it is bound to, the record kinds and verbs its write scope allows, and the spec records --current prints, read from the repository alone. Without name, this server's session (CHANT_AGENT). Returns the document unchanged.",
172
+ inputSchema: {
173
+ type: "object",
174
+ properties: {
175
+ name: { type: "string", description: "The session's name, as the declaration's agents list gives it." },
176
+ },
177
+ },
178
+ },
157
179
  {
158
180
  name: "workspace-work-history",
159
181
  description:
@@ -334,6 +356,10 @@ export function readArgv(tool: string, params: Record<string, unknown>): string[
334
356
  "--json",
335
357
  ];
336
358
  }
359
+ case "workspace-agent": {
360
+ const name = str(params, "name", true)!;
361
+ return ["workspace", "agent", name, "--json"];
362
+ }
337
363
  case "workspace-work-history": {
338
364
  const item = str(params, "item", true)!;
339
365
  const kind = str(params, "kind");
@@ -413,10 +439,12 @@ export interface WorkspaceTool {
413
439
  export function createWorkspaceTools(options: WorkspaceToolsOptions): WorkspaceTool[] {
414
440
  const { cwd } = options;
415
441
  const chant = options.chantCommand ?? ownChantCommand();
442
+ const agent = options.agent ?? (process.env[AGENT_ENV] || undefined);
416
443
  const reads: WorkspaceTool[] = workspaceReadTools.map((definition) => ({
417
444
  definition,
418
445
  handler: async (params) => {
419
- const argv = readArgv(definition.name, params);
446
+ const given = definition.name === "workspace-agent" && params.name === undefined && agent !== undefined ? { ...params, name: agent } : params;
447
+ const argv = readArgv(definition.name, given);
420
448
  const id = definition.name === "workspace-records" ? str(params, "id") : undefined;
421
449
  const doc = await runRead(chant, argv, cwd);
422
450
  return id !== undefined ? onlyRecord(doc, id) : doc;
@@ -446,6 +474,7 @@ export function createWorkspaceTools(options: WorkspaceToolsOptions): WorkspaceT
446
474
  sign: sign(params),
447
475
  dryRun: bool(params, "dryRun"),
448
476
  cwd,
477
+ agent,
449
478
  through: { source: mcpSource(context), opensInitial: true },
450
479
  });
451
480
  },
@@ -463,6 +492,7 @@ export function createWorkspaceTools(options: WorkspaceToolsOptions): WorkspaceT
463
492
  sign: sign(params),
464
493
  dryRun: bool(params, "dryRun"),
465
494
  cwd,
495
+ agent,
466
496
  through: { source: mcpSource(context) },
467
497
  });
468
498
  },
@@ -481,6 +511,7 @@ export function createWorkspaceTools(options: WorkspaceToolsOptions): WorkspaceT
481
511
  sign: sign(params),
482
512
  dryRun: bool(params, "dryRun"),
483
513
  cwd,
514
+ agent,
484
515
  });
485
516
  },
486
517
  "points-answer": async (params) => {
@@ -498,7 +529,7 @@ export function createWorkspaceTools(options: WorkspaceToolsOptions): WorkspaceT
498
529
  const named = str(params, "kind");
499
530
  const kind = named !== undefined ? w.resolveWriteKind(named, cwd) : await w.declaredSessionKind(RECORDS_CLOSE_SCHEMA_ID, cwd);
500
531
  if (typeof kind !== "string") return kind;
501
- return closeRecord({ kind, id, dryRun: bool(params, "dryRun"), cwd });
532
+ return closeRecord({ kind, id, dryRun: bool(params, "dryRun"), cwd, agent });
502
533
  },
503
534
  };
504
535
 
@@ -133,6 +133,16 @@ export interface ParsedArgs {
133
133
  namespace?: string;
134
134
  /** `chant workspace upgrade --allow-code` (#2550) — run migrations whose body is the template's own code. */
135
135
  allowCode?: boolean;
136
+ /** `chant workspace upgrade --source <repo>[#<member>]` (#2551): move the scope to another template. */
137
+ source?: string;
138
+ /** `chant workspace adopt-lineage --tags <glob>` and `hash-index --tags` (#2551): the tags to compare. */
139
+ tags?: string;
140
+ /** `chant workspace adopt-lineage --index <file>` (#2551): a cached hash index. */
141
+ index?: string;
142
+ /** `chant workspace versions --available` (#2551): list each git template's version tags. */
143
+ available?: boolean;
144
+ /** `chant workspace import --remove` (#2552): remove the export member instead of writing it again. */
145
+ remove?: boolean;
136
146
  /** `chant lifecycle rollback --dry-run` — compute the rollback delta and print it; open no PR, push nothing, leave no branch. */
137
147
  dryRun?: boolean;
138
148
  /** `chant lifecycle teardown <env> --yes` — execute the planned deletion
@@ -342,6 +352,18 @@ export interface ParsedArgs {
342
352
  prefix?: string;
343
353
  /** `chant workspace records|verify --require attested` (#2547): the provenance level a gate requires. */
344
354
  require?: string;
355
+ /** `chant workspace evidence sign --key` (a runner's Ed25519 PEM) and `signers sign --key` (an ssh private key) (#2553). */
356
+ key?: string;
357
+ /** `chant workspace evidence sign --check-id <id>` (#2553): the check the evidence is for. */
358
+ checkId?: string;
359
+ /** `chant workspace evidence sign --claim <file>` (#2553): hashed into the evidence. */
360
+ claim?: string;
361
+ /** `chant workspace evidence sign --environment <file>` (#2553): the runner environment, hashed into the evidence. */
362
+ environment?: string;
363
+ /** `chant workspace evidence verify --envelope <file>` (#2553). */
364
+ envelope?: string;
365
+ /** `chant workspace signers rotate --threshold <n>` (#2553). */
366
+ threshold?: string;
345
367
  /**
346
368
  * `chant build --root-only` and `chant lint --root-only` (#2537): run on the
347
369
  * root project of a declared workspace, members left out, instead of
@@ -70,7 +70,7 @@ describe("chant serve mcp at a workspace root with no lexicon of its own (#2700)
70
70
  expect(init.serverInfo.name).toBe("chant");
71
71
  for (const name of CORE_TOOLS) expect(tools).toContain(name);
72
72
  expect(tools).toContain("docker:diff");
73
- expect(init.instructions).toContain("Member delivery (delivery/) declares docker.");
73
+ expect(init.instructions).toContain("Member delivery (delivery/) declares docker, otel.");
74
74
  }, TIMEOUT);
75
75
 
76
76
  test("a lexiconless root, lexicons only in a member, serves core and the lexicons that load", () => {
@@ -36,6 +36,9 @@ function releaseTagShapes(): Array<{ recipe: string; example: string }> {
36
36
  // `git tag "chant-v$next"` / `git tag "lexicon-{{name}}-v$next"`
37
37
  for (const m of justfile.matchAll(/git tag "([^"]+)"/g)) {
38
38
  const raw = m[1];
39
+ // The reference workspace's tag (`release_reference_tag`, #2543) rides
40
+ // along with a chant release and publishes nothing by design.
41
+ if (raw === "$rtag") continue;
39
42
  const example = raw
40
43
  .replace(/\{\{name\}\}/g, "docker")
41
44
  .replace(/\$\{?next\}?/g, "9.9.9");
@@ -196,7 +199,8 @@ describe("release wiring: release the newest green commit (#2816)", () => {
196
199
  }
197
200
  const lib = readFileSync(join(repoRoot, "scripts", "release-lib.sh"), "utf-8");
198
201
  // main and the tag land together or not at all, and nothing is forced.
199
- expect(lib).toMatch(/git push --quiet --atomic origin "HEAD:refs\/heads\/main" "refs\/tags\/\$tag"/);
202
+ expect(lib).toMatch(/push_refs=\("refs\/tags\/\$tag"\)/);
203
+ expect(lib).toMatch(/git push --quiet --atomic origin "HEAD:refs\/heads\/main" "\$\{push_refs\[@\]\}"/);
200
204
  expect(lib).not.toMatch(/git push[^\n]*(--force|\s-f\b)/);
201
205
  });
202
206
 
@@ -93,7 +93,7 @@ describe("the derivation", () => {
93
93
  expect(out).toContain(`plan: ${plan.digest}`);
94
94
  expect(out).toContain(`chant approve fan-out release --plan ${plan.digest}`);
95
95
  // Nothing abbreviates it: the whole 64-hex string is on the line.
96
- expect(plan.digest).toMatch(/^sha256:[0-9a-f]{64}$/);
96
+ expect(plan.digest).toMatch(/^jcs1-sha256:[0-9a-f]{64}$/);
97
97
  });
98
98
 
99
99
  test("says so plainly when a change propagates to nothing", () => {
@@ -157,7 +157,7 @@ describe("the binding digest", () => {
157
157
  });
158
158
 
159
159
  test("it looks like every other plan digest, so `chant approve --plan` accepts it", () => {
160
- expect(planFanOut({ components: ESTATE, changed: ["net"] }).digest).toMatch(/^sha256:[0-9a-f]{64}$/);
160
+ expect(planFanOut({ components: ESTATE, changed: ["net"] }).digest).toMatch(/^jcs1-sha256:[0-9a-f]{64}$/);
161
161
  });
162
162
  });
163
163
 
@@ -492,7 +492,7 @@ describe("a redeploy's gate approval is bound like a rollback's (#2574, #2604)",
492
492
  const first = await runPromotion({ ...opts, now: "2026-01-05T00:00:00Z" });
493
493
  expect(first.status).toBe("gated");
494
494
  expect(first.gate?.environment).toBe("prod");
495
- expect(first.gate?.planDigest).toMatch(/^sha256:[0-9a-f]{64}$/);
495
+ expect(first.gate?.planDigest).toMatch(/^jcs1-sha256:[0-9a-f]{64}$/);
496
496
  expect(ran).toEqual([]);
497
497
 
498
498
  resolutions.push(approval({ planDigest: first.gate!.planDigest!, environment: "prod" }, "2026-01-05T00:01:00Z"));
package/src/config.ts CHANGED
@@ -283,6 +283,18 @@ export interface ChantConfig {
283
283
  /** Lint configuration (rules, extends, overrides, plugins) */
284
284
  lint?: LintConfig;
285
285
 
286
+ /**
287
+ * Telemetry attribution (#2558, D22): lexicons that declare workloads stamp
288
+ * resource attributes (`chant.workspace`, `chant.member`, `chant.decl`,
289
+ * `service.name`, `deployment.environment.name`) as `OTEL_*` environment
290
+ * variables. Inside a workspace this is on; `attribution: false` turns it
291
+ * off. Outside a workspace it is off, and `attribution: true` turns it on
292
+ * with the attributes that need no workspace. See `./telemetry-attribution.ts`.
293
+ */
294
+ telemetry?: {
295
+ attribution?: boolean;
296
+ };
297
+
286
298
  /**
287
299
  * Opt-in cloud-side ownership marking. When `stack` is set (and `enabled`
288
300
  * is not false), the serializer stamps a chant ownership marker carrying