@intentius/chant 0.86.0 → 0.88.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 (83) hide show
  1. package/dist/cli/handlers/misc.d.ts.map +1 -1
  2. package/dist/cli/main.d.ts.map +1 -1
  3. package/dist/cli/mcp/server.d.ts.map +1 -1
  4. package/dist/cli/registry.d.ts +5 -0
  5. package/dist/cli/registry.d.ts.map +1 -1
  6. package/dist/cli/version.d.ts +8 -0
  7. package/dist/cli/version.d.ts.map +1 -0
  8. package/dist/workspace/__fixtures__/sessions.d.ts +9 -0
  9. package/dist/workspace/__fixtures__/sessions.d.ts.map +1 -1
  10. package/dist/workspace/composites.d.ts +13 -0
  11. package/dist/workspace/composites.d.ts.map +1 -1
  12. package/dist/workspace/environments.d.ts +80 -0
  13. package/dist/workspace/environments.d.ts.map +1 -0
  14. package/dist/workspace/intent.d.ts +6 -1
  15. package/dist/workspace/intent.d.ts.map +1 -1
  16. package/dist/workspace/reason-codes.d.ts +20 -4
  17. package/dist/workspace/reason-codes.d.ts.map +1 -1
  18. package/dist/workspace/records-cli.d.ts +12 -2
  19. package/dist/workspace/records-cli.d.ts.map +1 -1
  20. package/dist/workspace/records-close.d.ts +41 -0
  21. package/dist/workspace/records-close.d.ts.map +1 -0
  22. package/dist/workspace/records-since.d.ts +40 -1
  23. package/dist/workspace/records-since.d.ts.map +1 -1
  24. package/dist/workspace/records-write.d.ts +117 -12
  25. package/dist/workspace/records-write.d.ts.map +1 -1
  26. package/dist/workspace/records.d.ts +84 -14
  27. package/dist/workspace/records.d.ts.map +1 -1
  28. package/dist/workspace/runtimes.d.ts +6 -0
  29. package/dist/workspace/runtimes.d.ts.map +1 -1
  30. package/dist/workspace/session-kinds.d.ts +28 -0
  31. package/dist/workspace/session-kinds.d.ts.map +1 -0
  32. package/dist/workspace/status.d.ts +2 -0
  33. package/dist/workspace/status.d.ts.map +1 -1
  34. package/dist/workspace/trust/seal.d.ts +127 -0
  35. package/dist/workspace/trust/seal.d.ts.map +1 -0
  36. package/dist/workspace/trust/ssh-commit.d.ts +7 -0
  37. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  38. package/dist/workspace/work.d.ts +3 -3
  39. package/package.json +1 -1
  40. package/src/cli/handlers/misc.ts +1 -9
  41. package/src/cli/main.ts +29 -10
  42. package/src/cli/mcp/server.test.ts +14 -1
  43. package/src/cli/mcp/server.ts +3 -1
  44. package/src/cli/registry.ts +5 -0
  45. package/src/cli/version.ts +15 -0
  46. package/src/workspace/__fixtures__/sessions.ts +41 -0
  47. package/src/workspace/composites.schema.json +68 -3
  48. package/src/workspace/composites.test.ts +119 -5
  49. package/src/workspace/composites.ts +26 -7
  50. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +23 -0
  51. package/src/workspace/environments.ts +165 -0
  52. package/src/workspace/intent-gaps.test.ts +217 -0
  53. package/src/workspace/intent.schema.json +23 -1
  54. package/src/workspace/intent.test.ts +2 -0
  55. package/src/workspace/intent.ts +35 -3
  56. package/src/workspace/read-contract.test.ts +3 -0
  57. package/src/workspace/reason-codes.test.ts +9 -3
  58. package/src/workspace/reason-codes.ts +24 -4
  59. package/src/workspace/record-assets.test.ts +4 -3
  60. package/src/workspace/records-amend.schema.json +30 -1
  61. package/src/workspace/records-cli.ts +113 -14
  62. package/src/workspace/records-close.schema.json +192 -0
  63. package/src/workspace/records-close.ts +129 -0
  64. package/src/workspace/records-contract.test.ts +4 -3
  65. package/src/workspace/records-new.schema.json +25 -0
  66. package/src/workspace/records-review.schema.json +81 -3
  67. package/src/workspace/records-sessions-write.test.ts +274 -0
  68. package/src/workspace/records-since.schema.json +27 -2
  69. package/src/workspace/records-since.ts +120 -6
  70. package/src/workspace/records-write-contract.test.ts +5 -1
  71. package/src/workspace/records-write.test.ts +4 -2
  72. package/src/workspace/records-write.ts +336 -43
  73. package/src/workspace/records.schema.json +37 -3
  74. package/src/workspace/records.ts +145 -25
  75. package/src/workspace/runtimes.ts +12 -3
  76. package/src/workspace/session-kinds.ts +79 -0
  77. package/src/workspace/status.ts +1 -1
  78. package/src/workspace/trust/record-seal.test.ts +315 -0
  79. package/src/workspace/trust/seal.test.ts +222 -0
  80. package/src/workspace/trust/seal.ts +289 -0
  81. package/src/workspace/trust/ssh-commit.ts +2 -2
  82. package/src/workspace/work.test.ts +3 -1
  83. package/src/workspace/work.ts +4 -4
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Sealed verdicts (#2687, part of #2547): a review entry signed by the
3
+ * principal it names, so the quorum counts it only when that principal
4
+ * signed it.
5
+ *
6
+ * A seal is a detached ssh signature, the same mechanism as the ssh-commit
7
+ * attestor (./ssh-commit.ts): `ssh-keygen -Y sign` to make it, and
8
+ * `ssh-keygen -Y verify` against the signers read at base to check it. It is
9
+ * made in its own namespace, `chant-review`, so neither a commit signature
10
+ * nor a signer-set signature can stand in for one.
11
+ *
12
+ * The signed bytes are the record id, the verdict's digest, the verdict, the
13
+ * reviewer and the date, each on its own line with no final newline. The
14
+ * digest is the record's text digest (`recordTextDigest`), so the seal binds
15
+ * the verdict to the text it judged. The seal lives inside the reviews block,
16
+ * which the digest leaves out, so sealing a verdict never moves the digest.
17
+ *
18
+ * A record's author seal (#2688) works the same way, in its own namespace,
19
+ * `chant-record`. It sits in the record's top-level `seal` field, which the
20
+ * digest also leaves out, and signs the record id, the digest, the author
21
+ * (the kind's `reviews.decider` field, `decided_by` for decisions) and the
22
+ * state. An amendment moves the digest, so it has to be signed again.
23
+ */
24
+ import type { SealCode } from "../records.js";
25
+ import { type TrustPolicy } from "./policy.js";
26
+ /** The ssh signature namespace of a verdict seal. */
27
+ export declare const REVIEW_SEAL_NAMESPACE = "chant-review";
28
+ /** The ssh signature namespace of a record's author seal (#2688). */
29
+ export declare const RECORD_SEAL_NAMESPACE = "chant-record";
30
+ /** A seal as a review entry holds it. */
31
+ export interface VerdictSeal {
32
+ /** The principal who signed: the reviewer, as the signers file names them. */
33
+ signer: string;
34
+ /** The signing key's fingerprint, such as `SHA256:...`. Reported, never trusted: the signature is the proof. */
35
+ key: string;
36
+ /** The armored ssh signature. */
37
+ signature: string;
38
+ }
39
+ /** What a verdict's seal establishes. */
40
+ export interface SealCheck {
41
+ /** true: the seal verifies for the reviewer against the signers at base. false: it is missing under an active policy, or it fails. null: nothing here can say. */
42
+ attested: boolean | null;
43
+ /** Why it is not attested. Absent when `attested` is true. */
44
+ code?: SealCode;
45
+ message: string;
46
+ /** The fingerprint of the key that made the signature, when the signature was checked. */
47
+ key?: string;
48
+ }
49
+ /** The verdict a seal covers. */
50
+ export interface SealedVerdict {
51
+ record: string | null;
52
+ reviewer: string;
53
+ verdict: string;
54
+ on: unknown;
55
+ digest: string | null;
56
+ seal: unknown;
57
+ }
58
+ /** The bytes a verdict seal signs. */
59
+ export declare function reviewSealPayload(record: string, digest: string, verdict: string, reviewer: string, on: string): Buffer;
60
+ /**
61
+ * The bytes a record's author seal signs (#2688): the id, the digest, the
62
+ * author and the state, joined by LF with no final newline. A kind without
63
+ * states signs an empty last line.
64
+ */
65
+ export declare function recordSealPayload(record: string, digest: string, author: string, state: string | null): Buffer;
66
+ /** The record an author seal covers (#2688). */
67
+ export interface SealedRecord {
68
+ record: string | null;
69
+ digest: string;
70
+ /** The author as the record names them, or null when it names none. */
71
+ author: string | null;
72
+ /** The kind's field that names the author, for messages. */
73
+ authorField: string;
74
+ state: string | null;
75
+ seal: unknown;
76
+ }
77
+ /**
78
+ * Check a verdict's seal. With a signers file active at base, it verifies
79
+ * against the keys listed there for the reviewer. With none, a seal present
80
+ * is checked for integrity only (`ssh-keygen -Y check-novalidate`): nothing
81
+ * says whose key it is.
82
+ */
83
+ export declare function checkVerdictSeal(policy: TrustPolicy, v: SealedVerdict): SealCheck;
84
+ /**
85
+ * Check a record's author seal (#2688), as a verdict's is checked, over
86
+ * {@link recordSealPayload} in the `chant-record` namespace. A record that
87
+ * names no author and carries no seal is `seal-missing` with `attested`
88
+ * null even under an active policy: it claims no author to attest.
89
+ */
90
+ export declare function checkRecordSeal(policy: TrustPolicy, r: SealedRecord): SealCheck;
91
+ /** Why a seal could not be made. */
92
+ export declare class SealError extends Error {
93
+ constructor(message: string);
94
+ }
95
+ /**
96
+ * The key `--sign` names: the file given, or with none, git's
97
+ * `user.signingkey` when `gpg.format` is `ssh`, as `git commit -S` reads it.
98
+ * A literal public key (`key::ssh-...`, or `ssh-...`) signs through the ssh
99
+ * agent holding its private half. Returns the file to pass to ssh-keygen,
100
+ * and a cleanup for a temporary one.
101
+ */
102
+ export declare function resolveSigningKey(sign: string | true, cwd: string): {
103
+ file: string;
104
+ cleanup: () => void;
105
+ };
106
+ /**
107
+ * Seal a verdict with the key in `keyFile` (a private key, or a public key
108
+ * whose private half the ssh agent holds). Throws a {@link SealError}.
109
+ */
110
+ export declare function sealVerdict(keyFile: string, v: {
111
+ record: string;
112
+ digest: string;
113
+ verdict: string;
114
+ reviewer: string;
115
+ on: string;
116
+ }): VerdictSeal;
117
+ /**
118
+ * Seal a record's author (#2688) with the key in `keyFile`, over
119
+ * {@link recordSealPayload}. Throws a {@link SealError}.
120
+ */
121
+ export declare function sealRecord(keyFile: string, r: {
122
+ record: string;
123
+ digest: string;
124
+ author: string;
125
+ state: string | null;
126
+ }): VerdictSeal;
127
+ //# sourceMappingURL=seal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seal.d.ts","sourceRoot":"","sources":["../../../src/workspace/trust/seal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAG5C,qDAAqD;AACrD,eAAO,MAAM,qBAAqB,iBAAiB,CAAC;AAEpD,qEAAqE;AACrE,eAAO,MAAM,qBAAqB,iBAAiB,CAAC;AAEpD,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;IACf,gHAAgH;IAChH,GAAG,EAAE,MAAM,CAAC;IACZ,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,yCAAyC;AACzC,MAAM,WAAW,SAAS;IACxB,kKAAkK;IAClK,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,sCAAsC;AACtC,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAEvH;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE9G;AAED,gDAAgD;AAChD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,OAAO,CAAC;CACf;AAoBD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,EAAE,aAAa,GAAG,SAAS,CAYjF;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,EAAE,YAAY,GAAG,SAAS,CAgB/E;AAuDD,oCAAoC;AACpC,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAuBzG;AAMD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,WAAW,CAE9I;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,WAAW,CAEpI"}
@@ -25,6 +25,13 @@ export interface SignedCommit {
25
25
  * continuation lines, to get the bytes it signed.
26
26
  */
27
27
  export declare function splitSignedCommit(raw: Buffer, header?: string): SignedCommit;
28
+ /** Run ssh-keygen; `missing` is true when it is not installed. Shared with the verdict seals (./seal.ts). */
29
+ export declare function sshKeygen(args: string[], input?: Buffer): {
30
+ status: number | null;
31
+ stdout: string;
32
+ stderr: string;
33
+ missing: boolean;
34
+ };
28
35
  /**
29
36
  * Check a detached ssh signature over `payload` in `namespace` against
30
37
  * `signers`. Shared with the rotation check (#2553), which signs signer sets
@@ -1 +1 @@
1
- {"version":3,"file":"ssh-commit.d.ts","sourceRoot":"","sources":["../../../src/workspace/trust/ssh-commit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAsC,cAAc,EAAE,MAAM,YAAY,CAAC;AACrF,OAAO,EAAwB,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AAI7D,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,SAAW,GAAG,YAAY,CAsB9E;AAmBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAuBjG;AAED,eAAO,MAAM,iBAAiB,EAAE,cAW/B,CAAC"}
1
+ {"version":3,"file":"ssh-commit.d.ts","sourceRoot":"","sources":["../../../src/workspace/trust/ssh-commit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAsC,cAAc,EAAE,MAAM,YAAY,CAAC;AACrF,OAAO,EAAwB,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AAI7D,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,SAAW,GAAG,YAAY,CAsB9E;AAUD,6GAA6G;AAC7G,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAIrI;AAID;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAuBjG;AAED,eAAO,MAAM,iBAAiB,EAAE,cAW/B,CAAC"}
@@ -17,9 +17,9 @@
17
17
  * - `implements`: each decision it names, with that decision's state;
18
18
  *
19
19
  * and each decision `implementedBy`, the work records naming it. The
20
- * warnings are closed codes. `work-done-gap-open` is not raised here: only
21
- * `graph --intent` walks a region, so only it can tell whether the finding a
22
- * done item came from still fires. It never writes a record.
20
+ * warnings are closed codes. `work-done-gap-open` is not raised here: it
21
+ * takes a walk of the item's region, which `graph --intent` makes and
22
+ * `records` asks it for (#2686). It never writes a record.
23
23
  */
24
24
  import { type LoadedRecordKind, type ReadRecordsOptions, type RecordEntry } from "./records.js";
25
25
  /** Why a work record carries a warning. Closed, like the record warning codes. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.86.0",
3
+ "version": "0.88.0",
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",
@@ -11,16 +11,8 @@ import { homedir } from "os";
11
11
  import type { ResourceSelector } from "../../lexicon";
12
12
  import { formatError, formatSuccess, formatWarning, formatBold } from "../format";
13
13
  import type { CommandContext } from "../registry";
14
- import { createRequire } from "module";
15
14
  import { listComponents, describeComponent } from "../../components/cli-support";
16
-
17
- const CHANT_VERSION: string = (() => {
18
- try {
19
- return createRequire(import.meta.url)("../../../package.json").version ?? "0.0.0";
20
- } catch {
21
- return "0.0.0";
22
- }
23
- })();
15
+ import { CHANT_VERSION } from "../version";
24
16
 
25
17
  const AUDIT_FORMATS: AuditFormat[] = ["stylish", "json", "sarif", "markdown", "html"];
26
18
  const AUDIT_TIERS: AuditTier[] = ["merge-worthy", "all"];
package/src/cli/main.ts CHANGED
@@ -387,6 +387,12 @@ export function parseArgs(args: string[]): ParsedArgs {
387
387
  // `chant workspace records review <id> --by <principal>` (#2670): whoever the caller says.
388
388
  result.by = args[++i];
389
389
  if (!result.by || result.by.startsWith("-")) throw new Error("--by needs the reviewer: --by <principal>");
390
+ } else if (arg === "--sign") {
391
+ // `chant workspace records review <id> --sign [<key file>]` (#2687): seal the verdict.
392
+ // `records new` and `records amend` take it too, to seal the record's author (#2688).
393
+ // With no key file, git's user.signingkey, as `git commit -S` reads it.
394
+ const next = args[i + 1];
395
+ result.sign = next !== undefined && !next.startsWith("-") ? args[++i] : true;
390
396
  } else if (arg === "--session") {
391
397
  // `chant workspace records review <id> --session <id>` (#2670)
392
398
  result.session = args[++i];
@@ -752,32 +758,45 @@ Workspace (level 1, #2524):
752
758
  --require attested exits 2 if any record is not
753
759
  attested. A pinned file that changed is a warning,
754
760
  asset-drift or asset-missing
755
- workspace records [--kind <kind file>] --since <rev> [--at <rev>] [--json]
761
+ workspace records [--kind <kind file>] --since <rev|session id> [--at <rev>] [--json]
756
762
  What changed in the records between <rev> and --at
757
763
  (default: the working tree): new and removed records,
758
764
  state transitions, new verdicts, new supersessions
759
- and changed pins
765
+ and changed pins. A session id compares the commits
766
+ the session opened and closed at
760
767
  workspace records pin <path>
761
768
  Print the path from the workspace root and the
762
769
  sha256 of a file, for a decision's evidence pin
763
- workspace records new [<kind file>] --from <file|-> [--prefix <prefix>] [--dry-run]
770
+ workspace records new [<kind file>] --from <file|-> [--prefix <prefix>] [--sign [<key file>]] [--dry-run]
764
771
  Write one new record in the kind's directory from
765
772
  the JSON fields given, after validating them as
766
773
  records would read them. Without a kind file, the one
767
774
  kind the declaration names. Allocates the next id when
768
- the fields hold none. Prints {path, id} as JSON and
769
- never commits
770
- workspace records amend <id> [--kind <kind file>] --set <file|-> [--dry-run]
775
+ the fields hold none. --sign seals the record's author
776
+ (decided_by for decisions) with an ssh key. Prints
777
+ {path, id} as JSON and never commits
778
+ workspace records amend <id> [--kind <kind file>] --set <file|-> [--sign [<key file>]] [--dry-run]
771
779
  Set top-level fields of one record. A closed record
772
780
  never changes, and an approved one changes only its
773
781
  state (upward), pins and reviews; anything else is
774
- refused with amend-supersede-instead. Prints
782
+ refused with amend-supersede-instead. --sign seals
783
+ the author again; without it an amendment removes
784
+ the author seal and says so. Prints
775
785
  {path, id, changed}
776
- workspace records review <id> [--kind <kind file>] --verdict agree|dissent|abstain --by <principal> [--note <text>] [--session <id>] [--dry-run]
786
+ workspace records review <id> [--kind <kind file>] --verdict agree|dissent|abstain --by <principal> [--note <text>] [--session <id>] [--sign [<key file>]] [--dry-run]
777
787
  Append a review to one record, dated and bound to
778
788
  the digest of the record text. A dissent needs
779
- --note. The principal is not checked; attestation is
780
- the seal's job. Prints {path, id, review}
789
+ --note. --sign seals it with an ssh key (git's
790
+ user.signingkey without a file); under a signers
791
+ file at base only a sealed verdict counts. With
792
+ --session, the session must be open, and the verdict
793
+ is appended to its verdicts too. Prints
794
+ {path, id, review}
795
+ workspace records close <session id> [--kind <session kind file>] [--dry-run]
796
+ Close an open review session: its state, close time,
797
+ closing commit and seal, in one write. Without
798
+ --kind, the one session kind the declaration names.
799
+ Prints {path, id, changed, seal, closedRev}
781
800
  workspace verify [--base <rev>] [--head <rev>] [--require attested]
782
801
  Check the commits in base..head against the signers
783
802
  and roles read from base. A change to the signers file
@@ -1,11 +1,15 @@
1
1
  import { describe, test, expect, beforeEach, afterEach } from "vitest";
2
2
  import { McpServer } from "./server";
3
+ import { readFileSync } from "node:fs";
3
4
  import { mkdir, rm, writeFile } from "node:fs/promises";
4
5
  import { join } from "node:path";
5
6
  import { tmpdir } from "node:os";
6
7
  import type { LexiconPlugin } from "../../lexicon";
7
8
  import type { Serializer } from "../../serializer";
8
9
 
10
+ /** The version in `packages/core/package.json`, read here apart from the code under test. */
11
+ const CORE_VERSION: string = JSON.parse(readFileSync(join(import.meta.dirname, "..", "..", "..", "package.json"), "utf-8")).version;
12
+
9
13
  function createMockPlugin(overrides?: Partial<LexiconPlugin>): LexiconPlugin {
10
14
  return {
11
15
  name: "mock",
@@ -51,7 +55,16 @@ describe("McpServer", () => {
51
55
  expect(result.protocolVersion).toBe("2026-07-28");
52
56
  expect(result.capabilities).toBeDefined();
53
57
  expect((result.serverInfo as Record<string, unknown>).name).toBe("chant");
54
- expect((result.serverInfo as Record<string, unknown>).version).toBe("0.1.0");
58
+ expect((result.serverInfo as Record<string, unknown>).version).toBe(CORE_VERSION);
59
+ });
60
+
61
+ test("server info names the installed chant's version, from core's package.json (#2689)", async () => {
62
+ expect(CORE_VERSION).toMatch(/^\d+\.\d+\.\d+/);
63
+ expect(CORE_VERSION).not.toBe("0.1.0");
64
+ for (const params of [{}, { protocolVersion: "2024-11-05" }]) {
65
+ const response = await server.handleRequest({ jsonrpc: "2.0", id: 1, method: "initialize", params });
66
+ expect(((response.result as Record<string, unknown>).serverInfo as Record<string, unknown>).version).toBe(CORE_VERSION);
67
+ }
55
68
  });
56
69
 
57
70
  test("capabilities include tools and resources", async () => {
@@ -13,6 +13,7 @@ import { createSnapshotTool, createDiffTool } from "./lifecycle-tools";
13
13
  import { setGateOrigin } from "../../lifecycle/gate-origin";
14
14
  import { createOpListTool, createOpRunTool, createOpStatusTool, createOpApproveTool, createOpReportTool } from "./op-tools";
15
15
  import { buildResourcesList, handleResourcesRead } from "./resource-handlers";
16
+ import { CHANT_VERSION } from "../version";
16
17
 
17
18
  /**
18
19
  * Protocol versions this server understands, newest first. `initialize` and
@@ -223,7 +224,8 @@ export class McpServer {
223
224
  return {
224
225
  protocolVersion: negotiateProtocolVersion(protocolVersion),
225
226
  capabilities: { tools: {}, resources: {} },
226
- serverInfo: { name: "chant", version: "0.1.0" },
227
+ // The installed chant's version, so a client can tell which chant it talks to (#2689).
228
+ serverInfo: { name: "chant", version: CHANT_VERSION },
227
229
  };
228
230
  }
229
231
 
@@ -297,6 +297,11 @@ export interface ParsedArgs {
297
297
  verdict?: string;
298
298
  /** `chant workspace records review <id> --by <principal>` (#2670): the reviewer, as the caller names them. */
299
299
  by?: string;
300
+ /**
301
+ * `chant workspace records review <id> --sign [<key file>]` (#2687): the key that seals the verdict, or true for git's user.signingkey.
302
+ * On `records new` and `records amend`, the key that seals the record's author (#2688).
303
+ */
304
+ sign?: string | true;
300
305
  /** `chant workspace records review <id> --session <id>` (#2670): the review session the verdict was given in. */
301
306
  session?: string;
302
307
  /** `chant workspace records new <kind> --prefix <prefix>` (#2670): the id prefix to allocate under. */
@@ -0,0 +1,15 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ /**
4
+ * The installed chant's version, read from `@intentius/chant`'s own
5
+ * package.json, or "0.0.0" when it can't be read. The path is the same from
6
+ * `src/cli/` and `dist/cli/`, so it holds whether chant runs from source or
7
+ * from its build.
8
+ */
9
+ export const CHANT_VERSION: string = (() => {
10
+ try {
11
+ return (createRequire(import.meta.url)("../../package.json") as { version?: string }).version ?? "0.0.0";
12
+ } catch {
13
+ return "0.0.0";
14
+ }
15
+ })();
@@ -64,3 +64,44 @@ export function reviewed(root: string, reviewers: string[], session: string, sta
64
64
  .replace(/^reviews: .*$/m, reviewers.length ? `reviews:\n${reviews}` : "reviews: []"),
65
65
  );
66
66
  }
67
+
68
+ /**
69
+ * Declare the fixture's two kinds in a chant.workspace.json (#2693): the
70
+ * decisions at the root and the session kind in the design member, as the
71
+ * reference workspace declares them, so review --session, close and
72
+ * --since <session id> find the session kind.
73
+ */
74
+ export function declareSessions(root: string): void {
75
+ writeFileSync(
76
+ join(root, "chant.workspace.json"),
77
+ `${JSON.stringify(
78
+ {
79
+ name: "sessions",
80
+ schema: 1,
81
+ members: [{ name: "design", dir: "design", kind: "other", because: "the session fixture", records: [{ kind: "sessions/session.kind.mjs" }] }],
82
+ records: [{ kind: DECISIONS_KIND }],
83
+ pins: [],
84
+ },
85
+ null,
86
+ 2,
87
+ )}\n`,
88
+ );
89
+ }
90
+
91
+ /** The fields of a new open session, as a UI sends them to records new. */
92
+ export function newSessionFields(over: Record<string, unknown> = {}): string {
93
+ return JSON.stringify({
94
+ schema: 1,
95
+ title: "Second walk",
96
+ state: "open",
97
+ agenda: [{ record: "ref-001" }, { record: "ref-002" }],
98
+ attendance: [
99
+ { principal: "lex00", class: "person" },
100
+ { principal: "alice", class: "person" },
101
+ ],
102
+ opened: "2026-09-25T09:00:00Z",
103
+ closed: null,
104
+ verdicts: [],
105
+ ...over,
106
+ });
107
+ }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://intentius.io/chant/schemas/workspace/composites/v1/composites.schema.json",
4
4
  "title": "chant workspace graph --composites output",
5
- "description": "What `chant workspace graph --composites` prints: each composite instance the workspace's members declare, joined to the components whose contract can deploy it (#2662). Each member of kind chant is read through its own `chant graph --format ir` and `chant graph --components --format ir`, under its own toolchain. An instance with no component is listed with an empty `components` array. chant supplies the rows and never picks one: each match says how it was made and how it crosses members. Readers ignore fields they do not know; a field is only ever added within a version. The reason and error codes are closed lists. Contract version 1 is written by chant 0.81.0 and newer, and this document was added to it by #2662; a chant without --composites refuses the flag. Each component lists the runtimes it can deploy on, read from its member's chant.config.ts (#2674). Every code is in the one closed list of `reason-codes.ts`.",
5
+ "description": "What `chant workspace graph --composites` prints: each composite instance the workspace's members declare, joined to the components whose contract can deploy it (#2662). Each member of kind chant is read through its own `chant graph --format ir` and `chant graph --components --format ir`, under its own toolchain. An instance with no component is listed with an empty `components` array. chant supplies the rows and never picks one: each match says how it was made and how it crosses members. Readers ignore fields they do not know; a field is only ever added within a version. The reason and error codes are closed lists. Contract version 1 is written by chant 0.81.0 and newer, and this document was added to it by #2662; a chant without --composites refuses the flag. Each component lists the runtimes it can deploy on, read from its member's chant.config.ts (#2674), and the environments it may deploy to, from that config and the member's release ledger on chant/lifecycle (#2695). Every code is in the one closed list of `reason-codes.ts`.",
6
6
  "oneOf": [
7
7
  {
8
8
  "$ref": "#/$defs/result"
@@ -131,7 +131,8 @@
131
131
  "status",
132
132
  "reason",
133
133
  "chant",
134
- "runtimeReasons"
134
+ "runtimeReasons",
135
+ "environmentReasons"
135
136
  ],
136
137
  "properties": {
137
138
  "name": {
@@ -210,6 +211,30 @@
210
211
  }
211
212
  }
212
213
  }
214
+ },
215
+ "environmentReasons": {
216
+ "description": "Why the environments of the member's components are only local, or leave out one its ledger has: its chant.config.ts could not be read (the same read and code as runtimeReasons), declares no environments, or doesn't cover an environment the ledger has, or the ledger couldn't be listed. Empty for a member not of kind chant, and when nothing needs saying.",
217
+ "type": "array",
218
+ "items": {
219
+ "type": "object",
220
+ "required": [
221
+ "code",
222
+ "message"
223
+ ],
224
+ "properties": {
225
+ "code": {
226
+ "enum": [
227
+ "runtimes-config-unreadable",
228
+ "environments-none-declared",
229
+ "environments-ledger-undeclared",
230
+ "environments-ledger-unreadable"
231
+ ]
232
+ },
233
+ "message": {
234
+ "type": "string"
235
+ }
236
+ }
237
+ }
213
238
  }
214
239
  },
215
240
  "if": {
@@ -351,7 +376,8 @@
351
376
  "archetype",
352
377
  "composites",
353
378
  "file",
354
- "runtimes"
379
+ "runtimes",
380
+ "environments"
355
381
  ],
356
382
  "properties": {
357
383
  "id": {
@@ -400,6 +426,14 @@
400
426
  "items": {
401
427
  "$ref": "#/$defs/runtime"
402
428
  }
429
+ },
430
+ "environments": {
431
+ "description": "The environments the component may deploy to with chant run --components --env in its member: local first, the default, then each name the member's chant.config.ts declares in environments (patterns left out), in config order, then each environment with a release ledger for the member on chant/lifecycle that the config covers, sorted. The component contract declares no environments.",
432
+ "type": "array",
433
+ "minItems": 1,
434
+ "items": {
435
+ "$ref": "#/$defs/environment"
436
+ }
403
437
  }
404
438
  }
405
439
  },
@@ -528,6 +562,37 @@
528
562
  "description": "The command that deploys the component on this runtime, run in the member's directory: chant run --components <name>, with --on <runtime> for any runtime but local."
529
563
  }
530
564
  }
565
+ },
566
+ "environment": {
567
+ "type": "object",
568
+ "required": [
569
+ "name",
570
+ "default",
571
+ "source",
572
+ "command"
573
+ ],
574
+ "properties": {
575
+ "name": {
576
+ "type": "string",
577
+ "description": "What --env takes."
578
+ },
579
+ "default": {
580
+ "type": "boolean",
581
+ "description": "True for the environment chant run --components deploys to without --env. That is local, since chant.config.ts names no default environment."
582
+ },
583
+ "source": {
584
+ "enum": [
585
+ "config",
586
+ "ledger",
587
+ "builtin"
588
+ ],
589
+ "description": "Where the name came from, the first of these that names it: config for the member's chant.config.ts environments, ledger for a release ledger on chant/lifecycle, builtin for local when neither names it."
590
+ },
591
+ "command": {
592
+ "type": "string",
593
+ "description": "The command that deploys the component to this environment on its default runtime, run in the member's directory: chant run --components <name>, with --on <runtime> when the default runtime isn't local, and --env <env> for any environment but the default."
594
+ }
595
+ }
531
596
  }
532
597
  }
533
598
  }