@intentius/chant 0.124.24 → 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 (96) hide show
  1. package/dist/cli/main.d.ts.map +1 -1
  2. package/dist/cli/main.js +21 -7
  3. package/dist/cli/mcp/workspace-tools.d.ts +3 -3
  4. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  5. package/dist/cli/mcp/workspace-tools.js +27 -4
  6. package/dist/effect-receipt.d.ts +13 -0
  7. package/dist/effect-receipt.d.ts.map +1 -1
  8. package/dist/effect-receipt.js +19 -0
  9. package/dist/lexicon-module.d.ts +7 -1
  10. package/dist/lexicon-module.d.ts.map +1 -1
  11. package/dist/lexicon-module.js +7 -2
  12. package/dist/lifecycle/receipt-plan.d.ts.map +1 -1
  13. package/dist/lifecycle/receipt-plan.js +4 -2
  14. package/dist/op/local-executor.d.ts.map +1 -1
  15. package/dist/op/local-executor.js +64 -5
  16. package/dist/op/receipt-store.d.ts +5 -0
  17. package/dist/op/receipt-store.d.ts.map +1 -1
  18. package/dist/op/receipt-store.js +4 -3
  19. package/dist/op/step-output-ref.d.ts +26 -18
  20. package/dist/op/step-output-ref.d.ts.map +1 -1
  21. package/dist/op/step-output-ref.js +86 -63
  22. package/dist/wave-runtime.mjs +213 -213
  23. package/dist/workspace/agent-cli.d.ts.map +1 -1
  24. package/dist/workspace/agent-cli.js +3 -0
  25. package/dist/workspace/conformance/__writer_fixture__/answers/answer.schema.json +16 -1
  26. package/dist/workspace/conformance/writer.js +1 -1
  27. package/dist/workspace/decide.d.ts +51 -9
  28. package/dist/workspace/decide.d.ts.map +1 -1
  29. package/dist/workspace/decide.js +125 -14
  30. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  31. package/dist/workspace/lineage-upgrade.js +20 -11
  32. package/dist/workspace/point-answer.schema.json +16 -1
  33. package/dist/workspace/points-cli.d.ts +5 -3
  34. package/dist/workspace/points-cli.d.ts.map +1 -1
  35. package/dist/workspace/points-cli.js +16 -7
  36. package/dist/workspace/points-write.schema.json +39 -4
  37. package/dist/workspace/points.schema.json +42 -3
  38. package/dist/workspace/reason-codes.d.ts +2 -1
  39. package/dist/workspace/reason-codes.d.ts.map +1 -1
  40. package/dist/workspace/reason-codes.js +2 -1
  41. package/dist/workspace/records-cli.d.ts +12 -2
  42. package/dist/workspace/records-cli.d.ts.map +1 -1
  43. package/dist/workspace/records-cli.js +27 -7
  44. package/dist/workspace/records-write.d.ts.map +1 -1
  45. package/dist/workspace/records-write.js +2 -3
  46. package/dist/workspace/records.d.ts +10 -4
  47. package/dist/workspace/records.d.ts.map +1 -1
  48. package/dist/workspace/records.js +16 -3
  49. package/dist/workspace/records.schema.json +2 -1
  50. package/dist/workspace/trust/verify-cli.d.ts.map +1 -1
  51. package/dist/workspace/trust/verify-cli.js +2 -1
  52. package/dist/workspace/trust/verify.d.ts +22 -0
  53. package/dist/workspace/trust/verify.d.ts.map +1 -1
  54. package/dist/workspace/trust/verify.js +94 -14
  55. package/dist/workspace/write-scope.d.ts +3 -1
  56. package/dist/workspace/write-scope.d.ts.map +1 -1
  57. package/dist/workspace/write-scope.js +11 -4
  58. package/package.json +1 -1
  59. package/src/cli/compiled-cli.e2e.test.ts +45 -1
  60. package/src/cli/main.test.ts +10 -0
  61. package/src/cli/main.ts +21 -7
  62. package/src/cli/mcp/workspace-tools.e2e.test.ts +1 -1
  63. package/src/cli/mcp/workspace-tools.ts +27 -4
  64. package/src/effect-receipt.ts +22 -0
  65. package/src/lexicon-module.ts +9 -2
  66. package/src/lifecycle/receipt-plan.test.ts +9 -0
  67. package/src/lifecycle/receipt-plan.ts +4 -1
  68. package/src/op/effect-step.test.ts +95 -1
  69. package/src/op/local-executor.test.ts +65 -0
  70. package/src/op/local-executor.ts +68 -5
  71. package/src/op/receipt-store.ts +13 -3
  72. package/src/op/step-output-ref.test.ts +68 -24
  73. package/src/op/step-output-ref.ts +85 -65
  74. package/src/workspace/agent-cli.ts +2 -0
  75. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +16 -1
  76. package/src/workspace/conformance/writer.ts +1 -1
  77. package/src/workspace/decide.test.ts +87 -3
  78. package/src/workspace/decide.ts +142 -17
  79. package/src/workspace/lineage-upgrade.test.ts +21 -0
  80. package/src/workspace/lineage-upgrade.ts +19 -11
  81. package/src/workspace/point-answer.schema.json +16 -1
  82. package/src/workspace/points-cli.ts +16 -8
  83. package/src/workspace/points-write.schema.json +39 -4
  84. package/src/workspace/points.schema.json +42 -3
  85. package/src/workspace/reason-codes.ts +2 -1
  86. package/src/workspace/record-assets.test.ts +14 -0
  87. package/src/workspace/records-cli.ts +28 -7
  88. package/src/workspace/records-write-commands.e2e.test.ts +32 -0
  89. package/src/workspace/records-write.ts +2 -3
  90. package/src/workspace/records.schema.json +2 -1
  91. package/src/workspace/records.ts +17 -4
  92. package/src/workspace/trust/trust.test.ts +105 -0
  93. package/src/workspace/trust/verify-cli.ts +2 -1
  94. package/src/workspace/trust/verify.ts +106 -13
  95. package/src/workspace/write-scope.test.ts +18 -0
  96. package/src/workspace/write-scope.ts +16 -5
@@ -131,8 +131,9 @@ export const SEAL_REASON_CODES = [
131
131
  "seal-unverifiable",
132
132
  ];
133
133
  /**
134
- * A warning about a record's own seal (#2688), raised by `records` with the
135
- * policy at base, never by a write. Closed, like the reason codes.
134
+ * A warning `records` raises with the policy at base, never by a write: about
135
+ * a record's own seal (#2688), or a ratified record's quorum (#2878). Closed,
136
+ * like the reason codes.
136
137
  */
137
138
  export const SEAL_WARNING_CODES = [
138
139
  /**
@@ -142,6 +143,13 @@ export const SEAL_WARNING_CODES = [
142
143
  * fails. The record is still read, and still valid.
143
144
  */
144
145
  "record-unattested",
146
+ /**
147
+ * The record is in its kind's ratified state (`reviews.ratified`, #2873),
148
+ * and its quorum, computed as `records` computes it, is not met: it was
149
+ * written to that state by hand, past the check of `records new` and
150
+ * `amend`. The record is still read, and still valid (#2878).
151
+ */
152
+ "record-ratified-below-quorum",
145
153
  ];
146
154
  /**
147
155
  * The top-level field that holds a record's author seal (#2688), on a kind
@@ -547,12 +555,17 @@ export async function readSessionSubjects(loaded, root, source) {
547
555
  * Import the kind file at `path` through the path loader lexicons use (#2520).
548
556
  * The kind is registered under a name no lexicon package can have (npm names
549
557
  * hold no `:`), imported and unregistered again, so no lexicon lookup sees it.
558
+ *
559
+ * A kind file is a data-only ES module, so a `.mjs`, `.cjs` or `.js` one loads
560
+ * with plain node and only a TypeScript one registers tsx (#3276). That keeps
561
+ * `workspace ls`, `status` and `records` from paying for tsx's loader thread
562
+ * on every read.
550
563
  */
551
564
  export async function importKindModule(path) {
552
565
  const name = `record-kind:${path}`;
553
566
  registerLexiconDeclarations([{ name, module: path }], dirname(path));
554
567
  try {
555
- const mod = await importLexiconModule(name);
568
+ const mod = await importLexiconModule(name, { typeScript: !/\.[cm]?js$/.test(path) });
556
569
  if (!mod)
557
570
  throw new Error(`${path} was not registered`);
558
571
  return mod;
@@ -413,7 +413,8 @@
413
413
  "answer-points-unreadable",
414
414
  "answer-point-unknown",
415
415
  "answer-point-changed",
416
- "record-unattested"
416
+ "record-unattested",
417
+ "record-ratified-below-quorum"
417
418
  ]
418
419
  },
419
420
  "message": { "type": "string" }
@@ -1 +1 @@
1
- {"version":3,"file":"verify-cli.d.ts","sourceRoot":"","sources":["../../../src/workspace/trust/verify-cli.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAOzD,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAqB7E"}
1
+ {"version":3,"file":"verify-cli.d.ts","sourceRoot":"","sources":["../../../src/workspace/trust/verify-cli.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAOzD,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAsB7E"}
@@ -22,6 +22,7 @@ export async function runWorkspaceVerify(ctx) {
22
22
  }
23
23
  const report = verifyChange({
24
24
  repo,
25
+ cwd: process.cwd(),
25
26
  base: args.base,
26
27
  head: args.head,
27
28
  require: args.require === "attested" || args.require === "attested-runs" ? args.require : undefined,
@@ -50,7 +51,7 @@ function printReport(r) {
50
51
  for (const c of r.runs.commits)
51
52
  lines.push(` ${c.commit.slice(0, 8)} agent run ${c.runs.join(", ") || c.run}: ${c.attested ? "attested" : "not attested"}, ${c.reason}`);
52
53
  for (const w of r.protectedWrites)
53
- lines.push(` protected write ${w.commit.slice(0, 8)} ${w.paths.join(", ")}: ${w.allowed ? "allowed" : "refused"}, ${w.reason}`);
54
+ lines.push(` protected write ${w.commit.slice(0, 8)} ${w.paths.join(", ")}${w.blocks ? ` (${w.blocks.join(", ")})` : ""}: ${w.allowed ? "allowed" : "refused"}, ${w.reason}`);
54
55
  if (r.rotation && "to" in r.rotation)
55
56
  lines.push(` signer set: version ${r.rotation.from} to ${r.rotation.to}, signed by ${r.rotation.signedBy.join(", ")}`);
56
57
  for (const n of r.notes)
@@ -8,6 +8,12 @@
8
8
  * that touches those files must be attested by a signer the base policy
9
9
  * trusts, and by an admin when the base policy grants the admin role.
10
10
  *
11
+ * The write scope is policy too (#3079): when the workspace declaration at
12
+ * base has a `writeScope` or `agents` block, a commit that changes either
13
+ * block, or removes the declaration, is a protected write on the same terms.
14
+ * Other edits to the declaration are not. The declaration is found from
15
+ * `cwd`, as `chant workspace check --changes` finds it.
16
+ *
11
17
  * Attestation is opt-in (#2525 rule 6). Without a signers file at base the
12
18
  * check passes and says so, unless `--require attested` asks for more.
13
19
  *
@@ -36,6 +42,8 @@ export interface CommitVerdict extends RecordProvenance {
36
42
  export interface ProtectedWrite {
37
43
  commit: string;
38
44
  paths: string[];
45
+ /** For a declaration write (#3079): the scope blocks the commit changes. */
46
+ blocks?: ScopeBlock[];
39
47
  level: ProvenanceLevel;
40
48
  principal?: string;
41
49
  allowed: boolean;
@@ -61,6 +69,16 @@ export interface ChangeReport {
61
69
  /** Protected paths whose content differs between the merge base and head. */
62
70
  protectedChanged: string[];
63
71
  protectedWrites: ProtectedWrite[];
72
+ /**
73
+ * The workspace declaration at base, when it has a scope block (#3079): its
74
+ * path from the repository root, the blocks it has, and the blocks that
75
+ * differ between the merge base and head.
76
+ */
77
+ declaration: {
78
+ path: string;
79
+ blocks: ScopeBlock[];
80
+ changed: ScopeBlock[];
81
+ } | null;
64
82
  /**
65
83
  * When the change edits the signer set (#2553): the version it proposes and
66
84
  * who signed it, or, when the new set is refused, the version it had to
@@ -81,6 +99,8 @@ export interface ChangeReport {
81
99
  }
82
100
  export interface VerifyOptions {
83
101
  repo: string;
102
+ /** Where to look for the workspace declaration, as `check --changes` does; the repository root by default. */
103
+ cwd?: string;
84
104
  base?: string;
85
105
  head?: string;
86
106
  require?: VerifyRequirement;
@@ -91,5 +111,7 @@ export interface VerifyOptions {
91
111
  * changes. Merges count only for what they change beyond every parent.
92
112
  */
93
113
  export declare function commitsTouching(repo: string, range: string, paths: string[]): Map<string, string[]>;
114
+ /** The declaration blocks whose change is a protected write (#3079). */
115
+ export type ScopeBlock = "writeScope" | "agents";
94
116
  export declare function verifyChange(opts: VerifyOptions): ChangeReport;
95
117
  //# sourceMappingURL=verify.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/workspace/trust/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAA+C,KAAK,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACnH,OAAO,EAAiC,KAAK,cAAc,EAAoB,MAAM,UAAU,CAAC;AAChG,OAAO,EAAoD,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACxG,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAE5D,8EAA8E;AAC9E,MAAM,MAAM,iBAAiB,GAAG,eAAe,GAAG,eAAe,CAAC;AAElE,MAAM,WAAW,aAAc,SAAQ,gBAAgB;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,eAAe,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,UAAU,GAAG,IAAI,CAAC;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE;QACN,MAAM,EAAE,OAAO,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,EAAE,CAAC;QACrB,QAAQ,EAAE,cAAc,EAAE,CAAC;QAC3B,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IACF,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAClC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,kGAAkG;IAClG,IAAI,EAAE,UAAU,CAAC;IACjB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,6EAA6E;IAC7E,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC;;;;OAIG;IACH,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,mBAAmB,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAChI,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,EAAE,EAAE,OAAO,CAAC;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;CACtC;AAMD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAUnG;AAQD,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAsI9D"}
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/workspace/trust/verify.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,12 @@
8
8
  * that touches those files must be attested by a signer the base policy
9
9
  * trusts, and by an admin when the base policy grants the admin role.
10
10
  *
11
+ * The write scope is policy too (#3079): when the workspace declaration at
12
+ * base has a `writeScope` or `agents` block, a commit that changes either
13
+ * block, or removes the declaration, is a protected write on the same terms.
14
+ * Other edits to the declaration are not. The declaration is found from
15
+ * `cwd`, as `chant workspace check --changes` finds it.
16
+ *
11
17
  * Attestation is opt-in (#2525 rule 6). Without a signers file at base the
12
18
  * check passes and says so, unless `--require attested` asks for more.
13
19
  *
@@ -21,6 +27,11 @@
21
27
  * `--require`, `runs` reports and nothing fails (studio-035 d).
22
28
  */
23
29
  import { execFileSync } from "node:child_process";
30
+ import { posix } from "node:path";
31
+ import { isDeepStrictEqual } from "node:util";
32
+ import { DECLARATION_FILES, WorkspaceReadError } from "../declaration.js";
33
+ import { parseJsonText } from "../jsonc.js";
34
+ import { locateWorkspace } from "../which-chant.js";
24
35
  import { policyAtBase, commitProvenance, resolveBase } from "./provenance.js";
25
36
  import { policyWriters, protectedPaths } from "./policy.js";
26
37
  import { fileAt, nextVersion, rotationPath, signerHistory } from "./rotation.js";
@@ -45,6 +56,47 @@ export function commitsTouching(repo, range, paths) {
45
56
  }
46
57
  return out;
47
58
  }
59
+ const SCOPE_BLOCKS = ["writeScope", "agents"];
60
+ function scopeState(repo, rev, dir) {
61
+ const present = DECLARATION_FILES.map((name) => ({ name, text: fileAt(repo, rev, posix.join(dir, name)) })).filter((f) => f.text !== undefined);
62
+ if (present.length === 0)
63
+ return { blocks: {} };
64
+ if (present.length > 1)
65
+ return { unreadable: present.map((f) => f.text).join("\0") };
66
+ const parsed = parseJsonText(present[0].text, { jsonc: present[0].name.endsWith(".jsonc") });
67
+ if (!parsed.ok || typeof parsed.value !== "object" || parsed.value === null || Array.isArray(parsed.value))
68
+ return { unreadable: present[0].text };
69
+ const obj = parsed.value;
70
+ return { blocks: Object.fromEntries(SCOPE_BLOCKS.filter((b) => obj[b] !== undefined).map((b) => [b, obj[b]])) };
71
+ }
72
+ /** The scope blocks that differ between two states; all of them when either can't be read and the texts differ. */
73
+ function changedBlocks(a, b) {
74
+ if ("unreadable" in a || "unreadable" in b) {
75
+ return "unreadable" in a && "unreadable" in b && a.unreadable === b.unreadable ? [] : [...SCOPE_BLOCKS];
76
+ }
77
+ return SCOPE_BLOCKS.filter((k) => !isDeepStrictEqual(a.blocks[k], b.blocks[k]));
78
+ }
79
+ /**
80
+ * The directory, from the repository root, of the declaration found from
81
+ * `cwd` at `base`, with the scope blocks it has there. Null when there is no
82
+ * declaration, or one with neither block: then nothing in it is protected. A
83
+ * declaration that can't be read at base counts as having both.
84
+ */
85
+ function scopedDeclarationAt(repo, cwd, base) {
86
+ let dir;
87
+ try {
88
+ const root = locateWorkspace(cwd, base).root;
89
+ dir = root === "." ? "" : root;
90
+ }
91
+ catch (err) {
92
+ if (err instanceof WorkspaceReadError)
93
+ return null;
94
+ throw err;
95
+ }
96
+ const state = scopeState(repo, base, dir);
97
+ const blocks = "unreadable" in state ? [...SCOPE_BLOCKS] : SCOPE_BLOCKS.filter((b) => b in state.blocks);
98
+ return blocks.length > 0 ? { dir, blocks } : null;
99
+ }
48
100
  /** Whether a merge commit changes nothing beyond what its parents bring. */
49
101
  function mergeWithoutOwnChanges(repo, commit, parents) {
50
102
  if (parents < 2)
@@ -73,6 +125,7 @@ export function verifyChange(opts) {
73
125
  protectedPaths: protectedPaths(policy),
74
126
  protectedChanged: [],
75
127
  protectedWrites: [],
128
+ declaration: null,
76
129
  rotation: null,
77
130
  notes: [],
78
131
  failures: [],
@@ -99,7 +152,8 @@ export function verifyChange(opts) {
99
152
  }
100
153
  // Every commit in the change, oldest first, with its parents.
101
154
  const listing = git(repo, ["rev-list", "--reverse", "--parents", `${base.commit}..${head}`]).split("\n").filter(Boolean);
102
- const parentsOf = new Map(listing.map((l) => { const [c, ...p] = l.split(" "); return [c, p.length]; }));
155
+ const parentList = new Map(listing.map((l) => { const [c, ...p] = l.split(" "); return [c, p]; }));
156
+ const parentsOf = new Map([...parentList].map(([c, p]) => [c, p.length]));
103
157
  report.runs = changeRuns(repo, [...parentsOf.keys()], policy.runners);
104
158
  if (opts.require === "attested-runs") {
105
159
  if (policy.runners.length === 0 && report.runs.commits.length > 0) {
@@ -145,24 +199,50 @@ export function verifyChange(opts) {
145
199
  report.commits.push(v);
146
200
  }
147
201
  // Protected writes: every commit that touches the policy needs a writer's signature.
202
+ const writers = policyWriters(policy);
203
+ const judge = (commit, paths, blocks) => {
204
+ const v = verdicts.get(commit) ?? { ...commitProvenance(repo, policy, commit, opts.attestors), commit, subject: "" };
205
+ const allowed = v.level === "attested" && v.principal !== undefined && writers.has(v.principal);
206
+ const who = policy.roles.admin?.length ? "an admin the policy at base names" : "a signer the policy at base lists";
207
+ const what = blocks ? `${paths.join(", ")} (${blocks.join(", ")})` : paths.join(", ");
208
+ const reason = allowed
209
+ ? `signed by ${v.principal}, ${who.replace(/^an? /, "")}`
210
+ : v.level === "attested"
211
+ ? `signed by ${v.principal}, who is not ${who}`
212
+ : `${what} is protected and needs a signature by ${who}: ${v.reason}`;
213
+ report.protectedWrites.push({ commit, paths, ...(blocks ? { blocks } : {}), level: v.level, ...(v.principal ? { principal: v.principal } : {}), allowed, reason });
214
+ if (!allowed)
215
+ report.failures.push(`${commit.slice(0, 8)} changes ${what}: ${reason}`);
216
+ };
148
217
  if (changedPaths.length > 0) {
149
- const writers = policyWriters(policy);
150
218
  const touching = commitsTouching(repo, `${base.commit}..${head}`, report.protectedPaths);
151
219
  if (touching.size === 0) {
152
220
  report.failures.push(`the policy files ${changedPaths.join(", ")} differ from base, and no commit in the change accounts for it`);
153
221
  }
154
- for (const [commit, paths] of touching) {
155
- const v = verdicts.get(commit) ?? { ...commitProvenance(repo, policy, commit, opts.attestors), commit, subject: "" };
156
- const allowed = v.level === "attested" && v.principal !== undefined && writers.has(v.principal);
157
- const who = policy.roles.admin?.length ? "an admin the policy at base names" : "a signer the policy at base lists";
158
- const reason = allowed
159
- ? `signed by ${v.principal}, ${who.replace(/^an? /, "")}`
160
- : v.level === "attested"
161
- ? `signed by ${v.principal}, who is not ${who}`
162
- : `${paths.join(", ")} is protected and needs a signature by ${who}: ${v.reason}`;
163
- report.protectedWrites.push({ commit, paths, level: v.level, ...(v.principal ? { principal: v.principal } : {}), allowed, reason });
164
- if (!allowed)
165
- report.failures.push(`${commit.slice(0, 8)} changes ${paths.join(", ")}: ${reason}`);
222
+ for (const [commit, paths] of touching)
223
+ judge(commit, paths);
224
+ }
225
+ // The write scope in the declaration at base (#3079): a commit that changes writeScope or agents.
226
+ const scoped = scopedDeclarationAt(repo, opts.cwd ?? repo, base.commit);
227
+ if (scoped) {
228
+ const files = DECLARATION_FILES.map((name) => posix.join(scoped.dir, name));
229
+ const changed = changedBlocks(scopeState(repo, mergeBase, scoped.dir), scopeState(repo, head, scoped.dir));
230
+ report.declaration = { path: files.find((f) => fileAt(repo, base.commit, f) !== undefined) ?? files[0], blocks: scoped.blocks, changed };
231
+ if (changed.length > 0) {
232
+ let accounted = false;
233
+ for (const [commit, paths] of commitsTouching(repo, `${base.commit}..${head}`, files)) {
234
+ const after = scopeState(repo, commit, scoped.dir);
235
+ // A merge changes a block only when it differs from every parent.
236
+ const per = (parentList.get(commit) ?? []).map((p) => new Set(changedBlocks(scopeState(repo, p, scoped.dir), after)));
237
+ const blocks = SCOPE_BLOCKS.filter((b) => per.length > 0 && per.every((s) => s.has(b)));
238
+ if (blocks.length === 0)
239
+ continue;
240
+ accounted = true;
241
+ judge(commit, paths, blocks);
242
+ }
243
+ if (!accounted) {
244
+ report.failures.push(`${changed.join(" and ")} in ${report.declaration.path} differ from base, and no commit in the change accounts for it`);
245
+ }
166
246
  }
167
247
  }
168
248
  // A new signer set must be signed by a threshold of the set at base (#2553).
@@ -37,7 +37,7 @@
37
37
  * `write-scope-class-unknown` until the plugin is installed at the pinned
38
38
  * version or the entry is removed.
39
39
  */
40
- import { type AgentDeclaration, type ClassScope, type Declaration, type PrincipalClass, type ProtectedPath, type WriteVerb } from "./declaration.js";
40
+ import { WorkspaceReadError, type AgentDeclaration, type ClassScope, type Declaration, type PrincipalClass, type ProtectedPath, type WriteVerb } from "./declaration.js";
41
41
  import { classesOf, roleGrants, type ClassRegistry } from "./principal-classes.js";
42
42
  import { type TrustPolicy } from "./trust/policy.js";
43
43
  import { AGENT_TRAILER } from "./trailers.js";
@@ -164,6 +164,8 @@ export interface ScopeSource {
164
164
  policy: TrustPolicy;
165
165
  /** The principal classes: the core four and those the declaration's pins supply, path pins read where the declaration was (#3080). */
166
166
  classes: ClassRegistry;
167
+ /** Why the working tree's declaration couldn't be read when `declaration` is null: declaration-missing, or declaration-invalid with the problem (#3433). */
168
+ failure?: WorkspaceReadError;
167
169
  }
168
170
  /**
169
171
  * The scope for a write from `cwd`: the declaration and the trust policy at
@@ -1 +1 @@
1
- {"version":3,"file":"write-scope.d.ts","sourceRoot":"","sources":["../../src/workspace/write-scope.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"write-scope.d.ts","sourceRoot":"","sources":["../../src/workspace/write-scope.ts"],"names":[],"mappings":"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"}
@@ -275,17 +275,22 @@ export function judgePath(declaration, writer, path, change) {
275
275
  message: `${who(writer)} may not write ${path}${allowance}: writeScope.${writer.class}.protected lists ${guarded.path}`,
276
276
  };
277
277
  }
278
- function readAt(cwd, at) {
278
+ /** The declaration at `at` (the working tree without it), or the read error that says why there is none. */
279
+ function readOrFailure(cwd, at) {
279
280
  try {
280
281
  const located = locateWorkspace(cwd, at);
281
282
  return { declaration: readDeclaration(located.tree), rootOnDisk: located.rootOnDisk, root: located.root, tree: located.tree };
282
283
  }
283
284
  catch (err) {
284
285
  if (err instanceof WorkspaceReadError)
285
- return null;
286
+ return err;
286
287
  throw err;
287
288
  }
288
289
  }
290
+ function readAt(cwd, at) {
291
+ const read = readOrFailure(cwd, at);
292
+ return read instanceof WorkspaceReadError ? null : read;
293
+ }
289
294
  /**
290
295
  * The scope for a write from `cwd`: the declaration and the trust policy at
291
296
  * the base revision (`origin/HEAD`, `main` or `master`), so a write can't
@@ -306,8 +311,10 @@ export function scopeSource(cwd, at) {
306
311
  if (atBase)
307
312
  return { ...withClasses(atBase), from: "base", policy };
308
313
  }
309
- const here = readAt(cwd);
310
- return here ? { ...withClasses(here), from: "working-tree", policy } : { declaration: null, from: null, rootOnDisk: null, root: null, policy, classes: coreClassRegistry() };
314
+ const here = readOrFailure(cwd);
315
+ if (here instanceof WorkspaceReadError)
316
+ return { declaration: null, from: null, rootOnDisk: null, root: null, policy, classes: coreClassRegistry(), failure: here };
317
+ return { ...withClasses(here), from: "working-tree", policy };
311
318
  }
312
319
  const toPosix = (p) => (sep === "/" ? p : p.split(sep).join("/"));
313
320
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.124.24",
3
+ "version": "0.124.25",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -7,7 +7,9 @@
7
7
  * Each run is traced through a load hook that NODE_OPTIONS hands to the CLI's
8
8
  * node, so the test can say what loaded: none of chant's TypeScript source,
9
9
  * ever, and tsx only for a command that imports TypeScript (here the
10
- * lexicon and the project source).
10
+ * lexicon and the project source). tsx's ESM hooks run on a thread of their
11
+ * own, about 200 MB of the process (#3276), so a workspace read must not
12
+ * register it either.
11
13
  *
12
14
  * End to end because it packs, installs from the registry and spawns the CLI.
13
15
  */
@@ -20,12 +22,14 @@ import { afterAll, beforeAll, describe, expect, test } from "vitest";
20
22
 
21
23
  const coreRoot = resolve(import.meta.dirname, "..", "..");
22
24
  const renderRoot = resolve(coreRoot, "..", "..", "lexicons", "render");
25
+ const referenceDecisions = resolve(coreRoot, "..", "..", "reference-workspace", "decisions");
23
26
  const version = (JSON.parse(readFileSync(join(coreRoot, "package.json"), "utf-8")) as { version: string }).version;
24
27
 
25
28
  let scratch: string;
26
29
  let project: string;
27
30
  let cjsProject: string;
28
31
  let waves: string;
32
+ let workspace: string;
29
33
  let chant: string;
30
34
  let tracer: string;
31
35
 
@@ -126,6 +130,31 @@ beforeAll(() => {
126
130
  writeFileSync(join(waves, ".gitignore"), ".chant/\n");
127
131
  git("add", "-A");
128
132
  git("commit", "-qm", "ignore");
133
+
134
+ // A workspace whose declaration names a .mjs record kind, with a member
135
+ // that has a chant.config.ts, for the workspace reads below (#3276).
136
+ workspace = join(scratch, "workspace");
137
+ mkdirSync(join(workspace, "decisions"), { recursive: true });
138
+ mkdirSync(join(workspace, "app"));
139
+ for (const file of ["decision.kind.mjs", "decision.schema.json", "ref-001-how-the-app-is-deployed.md"]) {
140
+ writeFileSync(join(workspace, "decisions", file), readFileSync(join(referenceDecisions, file)));
141
+ }
142
+ writeFileSync(
143
+ join(workspace, "chant.workspace.json"),
144
+ JSON.stringify({
145
+ name: "e2e",
146
+ schema: 1,
147
+ members: [{ name: "app", dir: "app", kind: "chant" }],
148
+ records: [{ kind: "decisions/decision.kind.mjs" }],
149
+ }),
150
+ );
151
+ writeFileSync(join(workspace, "app", "chant.config.ts"), `export default { lexicons: [] as string[] };\n`);
152
+ symlinkSync(join(project, "node_modules"), join(workspace, "node_modules"), "dir");
153
+ const wsGit = (...args: string[]) => execFileSync("git", ["-c", "user.email=e2e@chant", "-c", "user.name=e2e", ...args], { cwd: workspace, stdio: "ignore" });
154
+ wsGit("init", "-q", "-b", "main");
155
+ writeFileSync(join(workspace, ".gitignore"), "node_modules\n");
156
+ wsGit("add", "-A");
157
+ wsGit("commit", "-qm", "workspace");
129
158
  }, 600_000);
130
159
 
131
160
  afterAll(() => {
@@ -148,6 +177,21 @@ describe("the installed chant CLI (#3878)", () => {
148
177
  expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0 });
149
178
  });
150
179
 
180
+ test("workspace ls reads a .mjs record kind from a member with a chant.config.ts without loading tsx", () => {
181
+ const r = run(join(workspace, "app"), ["workspace", "ls", "--json"]);
182
+ expect(r.status, r.stderr).toBe(0);
183
+ const ls = JSON.parse(r.stdout) as { workspace: { records: Array<{ name: string; kind: string | null }> } };
184
+ expect(ls.workspace.records.map((k) => k.kind)).toEqual(["decision"]);
185
+ expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0 });
186
+ });
187
+
188
+ test("workspace records reads the declared .mjs kind's records without loading tsx", () => {
189
+ const r = run(join(workspace, "app"), ["workspace", "records", "--json"]);
190
+ expect(r.status, r.stderr).toBe(0);
191
+ expect(r.stdout).toContain("ref-001");
192
+ expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0 });
193
+ });
194
+
151
195
  test("build loads the TypeScript lexicon and project through tsx, against the one compiled core", () => {
152
196
  const r = run(project, ["build", "src", "-o", "out/render.json"]);
153
197
  expect(r.status, r.stderr).toBe(0);
@@ -1003,3 +1003,13 @@ describe("chant workspace check --generated (#2641)", () => {
1003
1003
  expect(() => parseArgs(["workspace", "check", "--generated=yes"])).toThrow();
1004
1004
  });
1005
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
@@ -402,9 +407,11 @@ Workspace (level 1, #2524):
402
407
  state transitions, new verdicts, new supersessions
403
408
  and changed pins. A session id compares the commits
404
409
  the session opened and closed at
405
- workspace records pin <path>
410
+ workspace records pin <path> [--kind <kind file>]
406
411
  Print the path from the workspace root and the
407
- 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
408
415
  workspace records new [<kind file>] --from <file|-> [--prefix <prefix>] [--sign [<key file>]] [--dry-run]
409
416
  Write one new record in the kind's directory from
410
417
  the JSON fields given, after validating them as
@@ -559,6 +566,11 @@ Workspace (level 1, #2524):
559
566
  workspace points retract <id> --by <name>... [--note <text>] [--kind <kind file>] [--dry-run]
560
567
  Take an answer back: the question is open for people
561
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)
562
574
  workspace pin <path> [--json]
563
575
  Print the integrity value that pins the plugin at
564
576
  <path>, to put in a path pin of chant.workspace.json.
@@ -1226,16 +1238,18 @@ export const commandRegistry: CommandDef[] = [
1226
1238
 
1227
1239
  // Workspace reads (#2524). Imported on first use, so a level-0 command never
1228
1240
  // loads anything under workspace/ (#2525 rule 5, pinned by #2526's goldens).
1229
- { 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) },
1230
1244
  // ws-058 (#2739) — decision points and the questions asked of them; never calls a model.
1231
- { 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) },
1232
1246
  { name: "workspace init", handler: async (ctx) => (await import("../workspace/init")).runWorkspaceInit(ctx) },
1233
- { 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) },
1234
1248
  // #2732 — the work lease: claim, renew and release a work item.
1235
1249
  { name: "workspace work", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/work-cli")).runWorkspaceWork(ctx) },
1236
1250
  // #3033 — the agent run record: what each run cost and which commits it made. chant records runs and never starts one.
1237
1251
  { name: "workspace runs", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/runs-cli")).runWorkspaceRuns(ctx) },
1238
- { 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) },
1239
1253
  { name: "workspace lineage", handler: async (ctx) => (await import("../workspace/lineage-cli")).runWorkspaceLineage(ctx) },
1240
1254
  { name: "workspace upgrade", handler: async (ctx) => (await import("../workspace/lineage-upgrade-cli")).runWorkspaceUpgrade(ctx) },
1241
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 () => {
@@ -15,9 +15,9 @@
15
15
  * read to the workspace root's pinned chant included (ws-021), and keeps
16
16
  * anything a command prints away from the protocol on stdout.
17
17
  * - The writes (`records-new`, `records-amend`, `records-review`,
18
- * `records-close`, `points-answer`, `points-retract`) call the functions
19
- * `chant workspace records new|amend|review|close` and `points
20
- * answer|retract` call, and return the
18
+ * `records-close`, `points-answer`, `points-retract`, `points-withdraw`)
19
+ * call the functions `chant workspace records new|amend|review|close` and
20
+ * `points answer|retract|withdraw` call, and return the
21
21
  * same JSON result. They keep every rule
22
22
  * the CLI keeps: the kind's schema, a closed record never changing, an
23
23
  * approved one changing only as its approval rule allows, a dissent needing
@@ -165,7 +165,7 @@ export const workspaceReadTools: ToolDefinition[] = [
165
165
  {
166
166
  name: "workspace-points",
167
167
  description:
168
- "The workspace's decision points and the questions asked of them: chant workspace points --json (points.schema.json, ws-058). A question is open while it is escalated to people, or proposed by a model and not yet confirmed; each lists any model's answer with its confidence and threshold. Returns the document unchanged. chant calls no model to answer this.",
168
+ "The workspace's decision points and the questions asked of them: chant workspace points --json (points.schema.json, ws-058). A question is open while it is escalated to people, or proposed by a model and not yet confirmed, and not withdrawn; each lists any model's answer with its confidence and threshold. Returns the document unchanged. chant calls no model to answer this.",
169
169
  inputSchema: {
170
170
  type: "object",
171
171
  properties: {
@@ -326,6 +326,23 @@ export const workspaceWriteTools: ToolDefinition[] = [
326
326
  required: ["id", "by"],
327
327
  },
328
328
  },
329
+ {
330
+ name: "points-withdraw",
331
+ description:
332
+ "Close an open decision point question that no longer applies, such as one about a work item its author changed, which a run asks about anew: chant workspace points withdraw (#2888). The question keeps its state, with who withdrew it, when and why; workspace-points open leaves it out, and points-answer refuses it. Asking it again opens it again. Withdrawing answers nothing, so it needs no quorum. An answered question is not withdrawn: points-retract takes the answer back. " +
333
+ PROTOCOL,
334
+ inputSchema: {
335
+ type: "object",
336
+ properties: {
337
+ id: { type: "string", description: "The question's id, as workspace-points lists it." },
338
+ by: { type: "array", items: { type: "string" }, description: "Who withdraws the question (--by)." },
339
+ note: { type: "string", description: "Why, such as the question that asks about the subject now (--note)." },
340
+ kind: { type: "string", description: "The answer kind file. Without it, the declared answer kinds." },
341
+ dryRun: dryRunProp,
342
+ },
343
+ required: ["id", "by"],
344
+ },
345
+ },
329
346
  ];
330
347
 
331
348
  /** A tool call that can't be made as given: the client gets it as an error result, and nothing runs. */
@@ -581,6 +598,12 @@ export function createWorkspaceTools(options: WorkspaceToolsOptions): WorkspaceT
581
598
  if (!Array.isArray(by) || by.length === 0 || !by.every((b) => typeof b === "string" && b.trim() !== "")) throw new ToolInputError("by must list each person who takes the answer back");
582
599
  return retractAnswer({ cwd, id: str(params, "id", true)!, by: by as string[], note: str(params, "note"), kind: str(params, "kind"), dryRun: bool(params, "dryRun") });
583
600
  },
601
+ "points-withdraw": async (params) => {
602
+ const { withdrawQuestion } = await import("../../workspace/decide");
603
+ const by = params.by;
604
+ if (!Array.isArray(by) || by.length === 0 || !by.every((b) => typeof b === "string" && b.trim() !== "")) throw new ToolInputError("by must name who withdraws the question");
605
+ return withdrawQuestion({ cwd, id: str(params, "id", true)!, by: by as string[], note: str(params, "note"), kind: str(params, "kind"), dryRun: bool(params, "dryRun") });
606
+ },
584
607
  "records-close": async (params) => {
585
608
  const w = await write();
586
609
  const { closeRecord, RECORDS_CLOSE_SCHEMA_ID } = await import("../../workspace/records-close");