@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
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Write scope per member and record kind, and agent sessions bound to one
3
+ * member (#2548, ws-067), on a workspace built in a throwaway git repository.
4
+ *
5
+ * main holds a declaration with two members, app and design, the workspace's
6
+ * own decision kind and a decision kind design declares, a trust policy that
7
+ * grants ci@example.com the runner role, and:
8
+ *
9
+ * - writeScope.agent: decisions may be proposed (new) and reviewed, not amended;
10
+ * - writeScope.runner: app's files only, and decision reviews only;
11
+ * - agents app-agent (app, listing bot@example.com) and design-agent (design).
12
+ *
13
+ * The record writes, the MCP tools, `check --changes` and `workspace agent`
14
+ * all read the scope from main, so a working-tree edit can't widen it.
15
+ */
16
+
17
+ import { readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
20
+ import { cleanScratch, contract, git, REPO, repo, writeFiles } from "./__fixtures__/contract-repo";
21
+ import { agentSession } from "./agent-cli";
22
+ import agentSchema from "./agent.schema.json";
23
+ import { checkChanges, type ChangesDocument } from "./changes";
24
+ import changesSchema from "./changes.schema.json";
25
+ import { formatChanges } from "./changes-cli";
26
+ import { parseDeclaration, WorkspaceReadError } from "./declaration";
27
+ import { parseFrontMatter } from "./records";
28
+ import { amendRecord, newRecord, renderRecord, reviewRecord } from "./records-write";
29
+ import { emptyPolicy } from "./trust/policy";
30
+ import { judgePath, judgeRecord, principalClass, resolveWriter, WriteScopeError } from "./write-scope";
31
+ import { createWorkspaceTools } from "../cli/mcp/workspace-tools";
32
+
33
+ const DECISIONS = join(REPO, "docs", "design", "decisions");
34
+ const KIND = "decisions/decision.kind.mjs";
35
+ const DESIGN_KIND = "design/decisions/decision.kind.mjs";
36
+
37
+ type Data = Record<string, unknown>;
38
+
39
+ const SAMPLE = (() => {
40
+ const fm = parseFrontMatter(readFileSync(join(DECISIONS, "ws-003-seal-scope.md"), "utf-8"));
41
+ if (!fm.ok) throw new Error(fm.message);
42
+ return fm.value;
43
+ })();
44
+
45
+ const decided = (over: Data): Data => ({ ...structuredClone(SAMPLE), ...over });
46
+ const proposal = (over: Data): Data => decided({ state: "proposed", choice: null, decided_by: null, decided_on: null, ...over });
47
+ const record = (d: Data) => renderRecord(d, `\n# ${String(d.title)}\n`);
48
+
49
+ const SCOPE = {
50
+ agent: { records: { decision: ["new", "review"] } },
51
+ runner: { members: ["app"], records: { decision: ["review"] } },
52
+ };
53
+ const AGENTS = [
54
+ { name: "app-agent", member: "app", principals: ["bot@example.com"] },
55
+ { name: "design-agent", member: "design" },
56
+ ];
57
+
58
+ function declaration(scope: unknown = SCOPE, agents: unknown = AGENTS): string {
59
+ return JSON.stringify(
60
+ {
61
+ name: "studio",
62
+ schema: 1,
63
+ members: [
64
+ { name: "app", dir: "app", kind: "other", because: "a plain Node server" },
65
+ { name: "design", dir: "design", kind: "other", because: "screens", records: [{ kind: "decisions/decision.kind.mjs", name: "screen" }] },
66
+ ],
67
+ records: [{ kind: KIND }],
68
+ writeScope: scope,
69
+ agents,
70
+ },
71
+ null,
72
+ 2,
73
+ );
74
+ }
75
+
76
+ let root: string;
77
+ const sha: Record<string, string> = {};
78
+
79
+ function commit(message: string, email?: string): string {
80
+ git(root, "add", "-A");
81
+ git(root, ...(email ? ["-c", `user.email=${email}`] : []), "commit", "-q", "-m", message);
82
+ return git(root, "rev-parse", "HEAD");
83
+ }
84
+
85
+ beforeAll(() => {
86
+ const kind = readFileSync(join(DECISIONS, "decision.kind.mjs"), "utf-8");
87
+ const schema = readFileSync(join(DECISIONS, "decision.schema.json"), "utf-8");
88
+ root = repo({
89
+ "chant.workspace.json": declaration(),
90
+ ".chant/trust.json": JSON.stringify({ schema: 1, roles: { runner: ["ci@example.com"] } }),
91
+ [KIND]: kind,
92
+ "decisions/decision.schema.json": schema,
93
+ "decisions/ws-001-one.md": record(decided({ id: "ws-001", title: "One" })),
94
+ [DESIGN_KIND]: kind,
95
+ "design/decisions/decision.schema.json": schema,
96
+ "design/decisions/ws-001-screen.md": record(decided({ id: "ws-001", title: "Screen" })),
97
+ "app/server.mjs": "export const port = 8080;\n",
98
+ "design/spec.md": "# Spec\n",
99
+ });
100
+ sha.base = commit("the workspace");
101
+ git(root, "branch", "-M", "main");
102
+ });
103
+ afterAll(cleanScratch);
104
+
105
+ const code = (doc: object) => ("error" in doc ? (doc as { error: { code: string } }).error.code : null);
106
+ const newDecision = (title: string) => JSON.stringify(proposal({ id: undefined, title }));
107
+
108
+ describe("the declaration's writeScope and agents (#2548)", () => {
109
+ test("are read with their defaults", () => {
110
+ const d = parseDeclaration(declaration(), "chant.workspace.json");
111
+ expect(d.writeScope).toEqual({
112
+ agent: { members: null, records: { decision: ["new", "review"] }, pointer: "/writeScope/agent" },
113
+ runner: { members: ["app"], records: { decision: ["review"] }, pointer: "/writeScope/runner" },
114
+ });
115
+ expect(d.agents.map((a) => [a.name, a.member, a.principals])).toEqual([
116
+ ["app-agent", "app", ["bot@example.com"]],
117
+ ["design-agent", "design", []],
118
+ ]);
119
+ const none = parseDeclaration(JSON.stringify({ name: "w", schema: 1, members: [] }), "chant.workspace.json");
120
+ expect([none.writeScope, none.agents]).toEqual([null, []]);
121
+ });
122
+
123
+ test.each([
124
+ ["an agent bound to no declared member", declaration(SCOPE, [{ name: "a", member: "nowhere" }]), /not a declared member/],
125
+ ["a scope naming no declared member", declaration({ human: { members: ["nowhere"] } }), /does not declare/],
126
+ ["a principal two sessions list", declaration(SCOPE, [{ name: "a", member: "app", principals: ["p"] }, { name: "b", member: "design", principals: ["p"] }]), /already listed by agent a/],
127
+ ["two sessions with one name", declaration(SCOPE, [{ name: "a", member: "app" }, { name: "a", member: "design" }]), /already used/],
128
+ ["a members list on the agent class", declaration({ agent: { members: ["app"] } }), /members/],
129
+ ["an unknown verb", declaration({ agent: { records: { decision: ["delete"] } } }), /must be equal to one of the allowed values|delete/],
130
+ ])("refuse %s", (_, text, message) => {
131
+ let err: unknown;
132
+ try {
133
+ parseDeclaration(text, "chant.workspace.json");
134
+ } catch (e) {
135
+ err = e;
136
+ }
137
+ expect(err).toBeInstanceOf(WorkspaceReadError);
138
+ expect((err as WorkspaceReadError).code).toBe("declaration-invalid");
139
+ expect((err as Error).message).toMatch(message);
140
+ });
141
+ });
142
+
143
+ describe("judging a write", () => {
144
+ const decl = parseDeclaration(declaration(), "chant.workspace.json");
145
+ const policy = { ...emptyPolicy("x"), roles: { runner: ["CI@example.com"], agent: ["model@example.com"] } };
146
+ const ws = { name: "decision", declaredName: null, member: null, declared: true };
147
+ const screen = { name: "decision", declaredName: "screen", member: "design", declared: true };
148
+
149
+ test("a principal's class comes from its role grants at base, and human is the rest", () => {
150
+ expect(principalClass(policy, "ci@example.com")).toBe("runner");
151
+ expect(principalClass(policy, "model@example.com")).toBe("agent");
152
+ expect(principalClass(policy, "lex00")).toBe("human");
153
+ expect(principalClass(policy, null)).toBe("human");
154
+ });
155
+
156
+ test("a session is named, or found by a principal it lists; an unknown one is refused", () => {
157
+ expect(resolveWriter(decl, policy, { agent: "app-agent" }).agent?.member).toBe("app");
158
+ expect(resolveWriter(decl, policy, { principal: "Bot@example.com" })).toMatchObject({ class: "agent", agent: { name: "app-agent" } });
159
+ expect(resolveWriter(decl, policy, { principal: "lex00" })).toMatchObject({ class: "human", agent: null });
160
+ expect(() => resolveWriter(decl, policy, { agent: "ghost" })).toThrow(WriteScopeError);
161
+ expect(() => resolveWriter(null, policy, { agent: "ghost" })).toThrow(/no workspace declaration/);
162
+ });
163
+
164
+ test("an agent writes its own member, the workspace's kinds as its records rule allows, and nothing of another member", () => {
165
+ const app = resolveWriter(decl, policy, { agent: "app-agent" });
166
+ expect(judgePath(decl, app, "app/server.mjs")).toEqual({ ok: true });
167
+ expect(judgePath(decl, app, "design/spec.md")).toMatchObject({ ok: false, code: "write-scope-member" });
168
+ expect(judgePath(decl, app, "chant.workspace.json")).toMatchObject({ ok: false, code: "write-scope-member", message: expect.stringContaining("in no member") });
169
+ expect(judgeRecord(decl, app, ws, "new")).toEqual({ ok: true });
170
+ expect(judgeRecord(decl, app, ws, "review")).toEqual({ ok: true });
171
+ expect(judgeRecord(decl, app, ws, "amend")).toMatchObject({ ok: false, code: "write-scope-kind", message: expect.stringContaining("may new, review decision records, and not amend") });
172
+ expect(judgeRecord(decl, app, ws, "delete")).toMatchObject({ ok: false, code: "write-scope-kind" });
173
+ expect(judgeRecord(decl, app, screen, "new")).toMatchObject({ ok: false, code: "write-scope-member" });
174
+ const design = resolveWriter(decl, policy, { agent: "design-agent" });
175
+ expect(judgeRecord(decl, design, screen, "new")).toEqual({ ok: true });
176
+ });
177
+
178
+ test("an agent by role with no session reaches only the workspace's own kinds", () => {
179
+ const model = resolveWriter(decl, policy, { principal: "model@example.com" });
180
+ expect(model).toMatchObject({ class: "agent", agent: null });
181
+ expect(judgePath(decl, model, "app/server.mjs")).toMatchObject({ ok: false, message: expect.stringContaining("which no agent session lists") });
182
+ expect(judgeRecord(decl, model, ws, "new")).toEqual({ ok: true });
183
+ });
184
+
185
+ test("a class with an entry is held to its members and kinds; a class with none is not restricted", () => {
186
+ const ci = resolveWriter(decl, policy, { principal: "ci@example.com" });
187
+ expect(judgePath(decl, ci, "app/server.mjs")).toEqual({ ok: true });
188
+ expect(judgePath(decl, ci, "design/spec.md")).toMatchObject({ ok: false, code: "write-scope-member" });
189
+ expect(judgeRecord(decl, ci, ws, "review")).toEqual({ ok: true });
190
+ expect(judgeRecord(decl, ci, ws, "new")).toMatchObject({ ok: false, code: "write-scope-kind", message: expect.stringContaining("may review decision records, and not new") });
191
+ expect(judgeRecord(decl, ci, screen, "review")).toMatchObject({ ok: false, code: "write-scope-member" });
192
+ const human = resolveWriter(decl, policy, { principal: "lex00" });
193
+ expect(judgePath(decl, human, "design/spec.md")).toEqual({ ok: true });
194
+ expect(judgeRecord(decl, human, screen, "delete")).toEqual({ ok: true });
195
+ });
196
+
197
+ test("the declared name of a kind is a key for its records rule too", () => {
198
+ const d = parseDeclaration(declaration({ agent: { records: { screen: ["new"] } } }), "chant.workspace.json");
199
+ const design = resolveWriter(d, policy, { agent: "design-agent" });
200
+ expect(judgeRecord(d, design, screen, "new")).toEqual({ ok: true });
201
+ expect(judgeRecord(d, design, ws, "new")).toMatchObject({ ok: false, code: "write-scope-kind", message: expect.stringContaining("does not list the kind") });
202
+ });
203
+ });
204
+
205
+ describe("the record writes refuse a write outside the writer's scope (CLI functions and MCP tools)", () => {
206
+ test("an agent session proposes and reviews a decision, and may not amend one", async () => {
207
+ const created = await newRecord({ kind: KIND, fields: newDecision("Logs"), cwd: root, agent: "app-agent", dryRun: true });
208
+ expect(code(created)).toBeNull();
209
+ const reviewed = await reviewRecord({ kind: KIND, id: "ws-001", verdict: "agree", by: "app-agent", cwd: root, agent: "app-agent", dryRun: true });
210
+ expect(code(reviewed)).toBeNull();
211
+ const amended = await amendRecord({ kind: KIND, id: "ws-001", fields: JSON.stringify({ state: "ratified" }), cwd: root, agent: "app-agent", dryRun: true });
212
+ expect(amended).toMatchObject({ error: { code: "write-scope-kind", message: expect.stringContaining("agent session app-agent, bound to member app,") } });
213
+ });
214
+
215
+ test("an agent session may not write another member's records", async () => {
216
+ expect(code(await newRecord({ kind: DESIGN_KIND, fields: newDecision("Screen two"), cwd: root, agent: "app-agent", dryRun: true }))).toBe("write-scope-member");
217
+ expect(code(await newRecord({ kind: DESIGN_KIND, fields: newDecision("Screen two"), cwd: root, agent: "design-agent", dryRun: true }))).toBeNull();
218
+ });
219
+
220
+ test("an unknown session is refused; a write naming no session is a human's, and a listed principal is its session's", async () => {
221
+ expect(code(await newRecord({ kind: KIND, fields: newDecision("Logs"), cwd: root, agent: "ghost", dryRun: true }))).toBe("agent-unknown");
222
+ expect(code(await amendRecord({ kind: KIND, id: "ws-001", fields: JSON.stringify({ title: "One, retitled" }), cwd: root, dryRun: true }))).toBe("amend-supersede-instead");
223
+ expect(code(await amendRecord({ kind: KIND, id: "ws-001", fields: JSON.stringify({ state: "ratified" }), by: "bot@example.com", cwd: root, dryRun: true }))).toBe("write-scope-kind");
224
+ });
225
+
226
+ test("a runner, by its role at base, reviews and does not propose", async () => {
227
+ expect(code(await reviewRecord({ kind: KIND, id: "ws-001", verdict: "agree", by: "ci@example.com", cwd: root, dryRun: true }))).toBeNull();
228
+ expect(code(await newRecord({ kind: KIND, fields: newDecision("Logs"), by: "ci@example.com", cwd: root, dryRun: true }))).toBe("write-scope-kind");
229
+ });
230
+
231
+ test("the scope is read at base: widening it in the working tree changes nothing until it is merged", async () => {
232
+ writeFiles(root, { "chant.workspace.json": declaration({ agent: { records: { decision: ["new", "review", "amend"] } } }) });
233
+ try {
234
+ expect(code(await amendRecord({ kind: KIND, id: "ws-001", fields: JSON.stringify({ state: "ratified" }), cwd: root, agent: "app-agent", dryRun: true }))).toBe("write-scope-kind");
235
+ } finally {
236
+ writeFiles(root, { "chant.workspace.json": declaration() });
237
+ }
238
+ });
239
+
240
+ test("the MCP record tools write in the server's session, with the CLI's codes", async () => {
241
+ const tools = createWorkspaceTools({ cwd: root, agent: "app-agent", chantCommand: ["false"] });
242
+ const tool = (name: string) => tools.find((t) => t.definition.name === name)!.handler;
243
+ const amended = (await tool("records-amend")({ id: "ws-001", kind: KIND, fields: { state: "ratified" }, dryRun: true })) as object;
244
+ expect(code(amended)).toBe("write-scope-kind");
245
+ const other = (await tool("records-new")({ kind: DESIGN_KIND, record: proposal({ id: undefined, title: "Screen two", state: undefined }), dryRun: true })) as object;
246
+ expect(code(other)).toBe("write-scope-member");
247
+ const own = (await tool("records-new")({ kind: KIND, record: proposal({ id: undefined, title: "Logs", state: undefined }), dryRun: true })) as object;
248
+ expect(code(own)).toBeNull();
249
+ });
250
+ });
251
+
252
+ describe("check --changes fails a commit outside its writer's scope (#2548)", () => {
253
+ const changes = contract(changesSchema);
254
+ let doc: Exclude<ChangesDocument, { error: unknown }>;
255
+ let failed: boolean;
256
+
257
+ beforeAll(async () => {
258
+ git(root, "checkout", "-q", "-b", "work");
259
+ const trailer = (name: string) => `\n\nChant-Agent: ${name}`;
260
+ writeFiles(root, { "app/server.mjs": "export const port = 9090;\n" });
261
+ sha.inMember = commit(`app: a port${trailer("app-agent")}`);
262
+ writeFiles(root, { "design/spec.md": "# Spec, by the app agent\n" });
263
+ sha.otherMember = commit(`design: an edit${trailer("app-agent")}`);
264
+ writeFiles(root, { "decisions/ws-002-logs.md": record(proposal({ id: "ws-002", title: "Logs" })) });
265
+ sha.proposed = commit(`a proposal${trailer("app-agent")}`);
266
+ writeFiles(root, { "decisions/ws-001-one.md": record(decided({ id: "ws-001", title: "One", reviews: [{ reviewer: "app-agent", verdict: "agree", on: "2026-09-30" }] })) });
267
+ sha.reviewed = commit(`a review${trailer("app-agent")}`);
268
+ writeFiles(root, { "decisions/ws-001-one.md": record(decided({ id: "ws-001", title: "One, decided again", reviews: [{ reviewer: "app-agent", verdict: "agree", on: "2026-09-30" }] })) });
269
+ sha.amended = commit(`an amendment${trailer("app-agent")}`);
270
+ writeFiles(root, { "chant.workspace.json": declaration({ agent: { records: { decision: ["new", "review", "amend"] } } }) });
271
+ sha.widened = commit(`widen my scope${trailer("app-agent")}`);
272
+ writeFiles(root, { "app/server.mjs": "export const port = 9191;\n" });
273
+ sha.ghost = commit(`a ghost${trailer("ghost")}`);
274
+ writeFiles(root, { "design/spec.md": "# Spec, by CI\n" });
275
+ sha.runner = commit("ci: an edit", "ci@example.com");
276
+ writeFiles(root, { "design/spec.md": "# Spec, by a person\n", "chant.workspace.json": declaration() });
277
+ sha.human = commit("a person's edit");
278
+ ({ doc, failed } = (await checkChanges({ cwd: root, range: "main..work" })) as { doc: Exclude<ChangesDocument, { error: unknown }>; failed: boolean });
279
+ });
280
+
281
+ test("the document validates and the check fails", () => {
282
+ changes.expectValid(doc);
283
+ expect(failed).toBe(true);
284
+ expect(doc.ok).toBe(false);
285
+ expect(doc.scope).toMatchObject({ base: sha.base, restricted: ["agent", "runner"], agents: ["app-agent", "design-agent"] });
286
+ });
287
+
288
+ test("each commit is judged as its session, its role, or a person", () => {
289
+ const by = Object.fromEntries(doc.scope!.commits.map((c) => [c.commit, [c.class, c.agent, c.principal, c.attested]]));
290
+ expect(by[sha.inMember]).toEqual(["agent", "app-agent", "t@example.com", false]);
291
+ expect(by[sha.ghost]).toEqual(["agent", "ghost", "t@example.com", false]);
292
+ expect(by[sha.runner]).toEqual(["runner", null, "ci@example.com", false]);
293
+ expect(by[sha.human]).toEqual(["human", null, "t@example.com", false]);
294
+ });
295
+
296
+ test("a finding for each write outside scope, and none for one inside it", () => {
297
+ const found = doc.scope!.findings.map((f) => [Object.entries(sha).find(([, s]) => s === f.commit)![0], f.code, f.path, f.verb]);
298
+ expect(found).toEqual([
299
+ ["otherMember", "write-scope-member", "design/spec.md", null],
300
+ ["amended", "write-scope-kind", "decisions/ws-001-one.md", "amend"],
301
+ ["widened", "write-scope-member", "chant.workspace.json", null],
302
+ ["ghost", "agent-unknown", null, null],
303
+ ["runner", "write-scope-member", "design/spec.md", null],
304
+ ]);
305
+ expect(formatChanges(doc)).toContain(`error write-scope-kind: ${sha.amended.slice(0, 8)} changes decisions/ws-001-one.md: agent session app-agent`);
306
+ });
307
+
308
+ test("an empty range judges no commit", async () => {
309
+ const { doc: before } = await checkChanges({ cwd: root, range: `${sha.base}~0..${sha.base}` });
310
+ expect("error" in before ? before : before.scope).toMatchObject({ base: sha.base, commits: [], findings: [] });
311
+ });
312
+ });
313
+
314
+ describe("chant workspace agent: what a session reloads from (#2548)", () => {
315
+ const agent = contract(agentSchema);
316
+
317
+ test("the session, its member, its scope and the spec, read from the repository", async () => {
318
+ const doc = await agentSession({ cwd: root, name: "design-agent" });
319
+ agent.expectValid(doc);
320
+ if ("error" in doc) throw new Error(doc.error.message);
321
+ expect(doc.workspace).toEqual({ name: "studio", root: ".", scopeFrom: "base" });
322
+ expect(doc.agent).toEqual({ name: "design-agent", member: { name: "design", dir: "design", kind: "other" }, principals: [] });
323
+ expect(doc.scope).toEqual({
324
+ members: ["design"],
325
+ records: [
326
+ { path: KIND, kind: "decision", name: null, member: null, verbs: ["new", "review"] },
327
+ { path: DESIGN_KIND, kind: "decision", name: "screen", member: "design", verbs: ["new", "review"] },
328
+ ],
329
+ });
330
+ expect(doc.spec.kinds.map((k) => k.path)).toEqual([KIND, DESIGN_KIND]);
331
+ expect(doc.spec.records.map((r) => `${r.kind}/${r.id}`)).toContain("decision/ws-001");
332
+ expect(doc.reload).toEqual(["chant workspace agent design-agent --json", "chant workspace records --current --json"]);
333
+ });
334
+
335
+ test("an unknown session is an error document", async () => {
336
+ const doc = await agentSession({ cwd: root, name: "ghost" });
337
+ agent.expectValid(doc);
338
+ expect(code(doc)).toBe("agent-unknown");
339
+ });
340
+ });