@cleocode/contracts 2026.6.12 → 2026.6.14

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 (216) hide show
  1. package/dist/__tests__/operation-def.test.js +5 -1
  2. package/dist/__tests__/operation-def.test.js.map +1 -1
  3. package/dist/__tests__/provider-def.test.d.ts +12 -0
  4. package/dist/__tests__/provider-def.test.d.ts.map +1 -0
  5. package/dist/__tests__/provider-def.test.js +84 -0
  6. package/dist/__tests__/provider-def.test.js.map +1 -0
  7. package/dist/__tests__/sealed-credential.test.d.ts +19 -0
  8. package/dist/__tests__/sealed-credential.test.d.ts.map +1 -0
  9. package/dist/__tests__/sealed-credential.test.js +74 -0
  10. package/dist/__tests__/sealed-credential.test.js.map +1 -0
  11. package/dist/__tests__/service-provider.test.d.ts +17 -0
  12. package/dist/__tests__/service-provider.test.d.ts.map +1 -0
  13. package/dist/__tests__/service-provider.test.js +108 -0
  14. package/dist/__tests__/service-provider.test.js.map +1 -0
  15. package/dist/__tests__/skill-executor-contract.test.js +3 -0
  16. package/dist/__tests__/skill-executor-contract.test.js.map +1 -1
  17. package/dist/acceptance-gate.d.ts +2 -3
  18. package/dist/acceptance-gate.d.ts.map +1 -1
  19. package/dist/boundary.d.ts +52 -0
  20. package/dist/boundary.d.ts.map +1 -1
  21. package/dist/boundary.js +24 -19
  22. package/dist/boundary.js.map +1 -1
  23. package/dist/conduit.d.ts +171 -0
  24. package/dist/conduit.d.ts.map +1 -1
  25. package/dist/config/index.d.ts +23 -0
  26. package/dist/config/index.d.ts.map +1 -0
  27. package/dist/config/index.js +22 -0
  28. package/dist/config/index.js.map +1 -0
  29. package/dist/config.d.ts +104 -1
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/data-accessor.d.ts +21 -1
  32. package/dist/data-accessor.d.ts.map +1 -1
  33. package/dist/data-accessor.js.map +1 -1
  34. package/dist/dispatch/__tests__/tasks-gateway-crud-ops.test.d.ts +21 -0
  35. package/dist/dispatch/__tests__/tasks-gateway-crud-ops.test.d.ts.map +1 -0
  36. package/dist/dispatch/__tests__/tasks-gateway-crud-ops.test.js +84 -0
  37. package/dist/dispatch/__tests__/tasks-gateway-crud-ops.test.js.map +1 -0
  38. package/dist/dispatch/identity.d.ts +1 -1
  39. package/dist/dispatch/identity.d.ts.map +1 -1
  40. package/dist/dispatch/identity.js +25 -0
  41. package/dist/dispatch/identity.js.map +1 -1
  42. package/dist/dispatch/operation-def.d.ts +20 -0
  43. package/dist/dispatch/operation-def.d.ts.map +1 -1
  44. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  45. package/dist/dispatch/operations-registry.js +1044 -182
  46. package/dist/dispatch/operations-registry.js.map +1 -1
  47. package/dist/docs/read.d.ts +2 -2
  48. package/dist/index.d.ts +25 -11
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +17 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/lease-ipc/__tests__/freeze.test.d.ts +15 -0
  53. package/dist/lease-ipc/__tests__/freeze.test.d.ts.map +1 -0
  54. package/dist/lease-ipc/__tests__/freeze.test.js +313 -0
  55. package/dist/lease-ipc/__tests__/freeze.test.js.map +1 -0
  56. package/dist/lease-ipc/index.d.ts +17 -0
  57. package/dist/lease-ipc/index.d.ts.map +1 -0
  58. package/dist/lease-ipc/index.js +16 -0
  59. package/dist/lease-ipc/index.js.map +1 -0
  60. package/dist/lease-ipc/messages.d.ts +918 -0
  61. package/dist/lease-ipc/messages.d.ts.map +1 -0
  62. package/dist/lease-ipc/messages.js +476 -0
  63. package/dist/lease-ipc/messages.js.map +1 -0
  64. package/dist/lease-ipc/version.d.ts +41 -0
  65. package/dist/lease-ipc/version.d.ts.map +1 -0
  66. package/dist/lease-ipc/version.js +43 -0
  67. package/dist/lease-ipc/version.js.map +1 -0
  68. package/dist/llm/catalog-schema.d.ts +242 -0
  69. package/dist/llm/catalog-schema.d.ts.map +1 -0
  70. package/dist/llm/catalog-schema.js +153 -0
  71. package/dist/llm/catalog-schema.js.map +1 -0
  72. package/dist/llm/catalog-schema.json +139 -0
  73. package/dist/llm/index.d.ts +15 -0
  74. package/dist/llm/index.d.ts.map +1 -0
  75. package/dist/llm/index.js +14 -0
  76. package/dist/llm/index.js.map +1 -0
  77. package/dist/llm/interfaces.d.ts +23 -0
  78. package/dist/llm/interfaces.d.ts.map +1 -1
  79. package/dist/llm/oauth.d.ts +16 -2
  80. package/dist/llm/oauth.d.ts.map +1 -1
  81. package/dist/llm/onboarding.d.ts +126 -0
  82. package/dist/llm/onboarding.d.ts.map +1 -0
  83. package/dist/llm/onboarding.js +34 -0
  84. package/dist/llm/onboarding.js.map +1 -0
  85. package/dist/llm/provider-def.d.ts +254 -0
  86. package/dist/llm/provider-def.d.ts.map +1 -0
  87. package/dist/llm/provider-def.js +93 -0
  88. package/dist/llm/provider-def.js.map +1 -0
  89. package/dist/llm/sealed-credential.d.ts +164 -0
  90. package/dist/llm/sealed-credential.d.ts.map +1 -0
  91. package/dist/llm/sealed-credential.js +43 -0
  92. package/dist/llm/sealed-credential.js.map +1 -0
  93. package/dist/llm/system-of-use.d.ts +383 -0
  94. package/dist/llm/system-of-use.d.ts.map +1 -0
  95. package/dist/llm/system-of-use.js +238 -0
  96. package/dist/llm/system-of-use.js.map +1 -0
  97. package/dist/llm/system-resolver.d.ts +87 -11
  98. package/dist/llm/system-resolver.d.ts.map +1 -1
  99. package/dist/llm/system-resolver.js +14 -7
  100. package/dist/llm/system-resolver.js.map +1 -1
  101. package/dist/operations/__tests__/ensures-schema-registry.test.d.ts +17 -0
  102. package/dist/operations/__tests__/ensures-schema-registry.test.d.ts.map +1 -0
  103. package/dist/operations/__tests__/ensures-schema-registry.test.js +113 -0
  104. package/dist/operations/__tests__/ensures-schema-registry.test.js.map +1 -0
  105. package/dist/operations/admin.d.ts +124 -1
  106. package/dist/operations/admin.d.ts.map +1 -1
  107. package/dist/operations/ensures-schema-registry.d.ts +172 -0
  108. package/dist/operations/ensures-schema-registry.d.ts.map +1 -0
  109. package/dist/operations/ensures-schema-registry.js +226 -0
  110. package/dist/operations/ensures-schema-registry.js.map +1 -0
  111. package/dist/operations/entities.d.ts +110 -0
  112. package/dist/operations/entities.d.ts.map +1 -0
  113. package/dist/operations/entities.js +695 -0
  114. package/dist/operations/entities.js.map +1 -0
  115. package/dist/operations/llm.d.ts +116 -9
  116. package/dist/operations/llm.d.ts.map +1 -1
  117. package/dist/operations/output-contracts-data.d.ts.map +1 -1
  118. package/dist/operations/output-contracts-data.js +230 -0
  119. package/dist/operations/output-contracts-data.js.map +1 -1
  120. package/dist/operations/service.d.ts +68 -0
  121. package/dist/operations/service.d.ts.map +1 -0
  122. package/dist/operations/service.js +336 -0
  123. package/dist/operations/service.js.map +1 -0
  124. package/dist/operations/tasks.d.ts +109 -0
  125. package/dist/operations/tasks.d.ts.map +1 -1
  126. package/dist/operations/tasks.js.map +1 -1
  127. package/dist/operations/validate.d.ts +1 -12
  128. package/dist/operations/validate.d.ts.map +1 -1
  129. package/dist/operations/validate.js +1 -1
  130. package/dist/playbook.d.ts +116 -0
  131. package/dist/playbook.d.ts.map +1 -1
  132. package/dist/release/plan.d.ts +5 -5
  133. package/dist/session.d.ts +11 -0
  134. package/dist/session.d.ts.map +1 -1
  135. package/dist/session.js.map +1 -1
  136. package/dist/templates/manifest.d.ts +1 -1
  137. package/dist/tools/agent-tools.d.ts +231 -0
  138. package/dist/tools/agent-tools.d.ts.map +1 -0
  139. package/dist/tools/agent-tools.js +25 -0
  140. package/dist/tools/agent-tools.js.map +1 -0
  141. package/dist/tools/atomic.d.ts +19 -0
  142. package/dist/tools/atomic.d.ts.map +1 -1
  143. package/dist/tools/atomic.js +17 -0
  144. package/dist/tools/atomic.js.map +1 -1
  145. package/dist/tools/index.d.ts +2 -0
  146. package/dist/tools/index.d.ts.map +1 -1
  147. package/dist/tools/skill-executor.d.ts +7 -0
  148. package/dist/tools/skill-executor.d.ts.map +1 -1
  149. package/dist/tools/web-tools.d.ts +217 -0
  150. package/dist/tools/web-tools.d.ts.map +1 -0
  151. package/dist/tools/web-tools.js +31 -0
  152. package/dist/tools/web-tools.js.map +1 -0
  153. package/dist/vault/service-provider.d.ts +343 -0
  154. package/dist/vault/service-provider.d.ts.map +1 -0
  155. package/dist/vault/service-provider.js +857 -0
  156. package/dist/vault/service-provider.js.map +1 -0
  157. package/dist/workgraph.d.ts +12 -12
  158. package/package.json +14 -2
  159. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  160. package/src/__tests__/operation-def.test.ts +7 -1
  161. package/src/__tests__/provider-def.test.ts +93 -0
  162. package/src/__tests__/sealed-credential.test.ts +100 -0
  163. package/src/__tests__/service-provider.test.ts +129 -0
  164. package/src/__tests__/skill-executor-contract.test.ts +3 -0
  165. package/src/acceptance-gate.ts +2 -3
  166. package/src/boundary.ts +70 -20
  167. package/src/conduit.ts +200 -0
  168. package/src/config/index.ts +35 -0
  169. package/src/config.ts +106 -1
  170. package/src/data-accessor.ts +22 -1
  171. package/src/dispatch/__tests__/tasks-gateway-crud-ops.test.ts +99 -0
  172. package/src/dispatch/identity.ts +25 -0
  173. package/src/dispatch/operation-def.ts +21 -0
  174. package/src/dispatch/operations-registry.ts +1083 -176
  175. package/src/index.ts +240 -25
  176. package/src/lease-ipc/__tests__/freeze.test.ts +342 -0
  177. package/src/lease-ipc/index.ts +75 -0
  178. package/src/lease-ipc/messages.ts +588 -0
  179. package/src/lease-ipc/version.ts +46 -0
  180. package/src/llm/catalog-schema.json +139 -0
  181. package/src/llm/catalog-schema.ts +184 -0
  182. package/src/llm/index.ts +29 -0
  183. package/src/llm/interfaces.ts +23 -0
  184. package/src/llm/oauth.ts +16 -2
  185. package/src/llm/onboarding.ts +131 -0
  186. package/src/llm/provider-def.ts +291 -0
  187. package/src/llm/sealed-credential.ts +166 -0
  188. package/src/llm/system-of-use.ts +503 -0
  189. package/src/llm/system-resolver.ts +91 -11
  190. package/src/operations/__tests__/ensures-schema-registry.test.ts +169 -0
  191. package/src/operations/admin.ts +144 -0
  192. package/src/operations/ensures-schema-registry.ts +260 -0
  193. package/src/operations/entities.ts +758 -0
  194. package/src/operations/llm.ts +120 -8
  195. package/src/operations/output-contracts-data.ts +264 -0
  196. package/src/operations/service.ts +360 -0
  197. package/src/operations/tasks.ts +119 -0
  198. package/src/operations/validate.ts +1 -14
  199. package/src/playbook.ts +117 -0
  200. package/src/session.ts +11 -0
  201. package/src/tools/agent-tools.ts +270 -0
  202. package/src/tools/atomic.ts +21 -0
  203. package/src/tools/index.ts +46 -0
  204. package/src/tools/skill-executor.ts +7 -0
  205. package/src/tools/web-tools.ts +247 -0
  206. package/src/vault/service-provider.ts +1137 -0
  207. package/dist/tessera.d.ts +0 -32
  208. package/dist/tessera.d.ts.map +0 -1
  209. package/dist/tessera.js +0 -11
  210. package/dist/tessera.js.map +0 -1
  211. package/dist/warp-chain.d.ts +0 -129
  212. package/dist/warp-chain.d.ts.map +0 -1
  213. package/dist/warp-chain.js +0 -13
  214. package/dist/warp-chain.js.map +0 -1
  215. package/src/tessera.ts +0 -35
  216. package/src/warp-chain.ts +0 -185
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Tests for the ensures-schema Zod registry (T11762 ST-1 · T11900).
3
+ *
4
+ * Behavior-parity guard: every input the deleted bespoke validators
5
+ * (`validateDecompositionTaskTree` / `validateIvtrEvidenceOutput`,
6
+ * `packages/playbooks/src/runtime.ts:794-902`) REJECTED must still be rejected
7
+ * here, with the FIRST Zod issue message equal to the SUFFIX of the original
8
+ * violation string (the part after the `ensures.schema[<name>] on <nodeId>: `
9
+ * prefix that ST-2's runtime wrapper re-applies). Every input they ACCEPTED must
10
+ * still parse. The ACs call out: empty array, missing title, empty acceptance,
11
+ * `{}` evidence, numeric evidence.
12
+ *
13
+ * @task T11900
14
+ * @epic T11762
15
+ */
16
+
17
+ import { describe, expect, it } from 'vitest';
18
+ import {
19
+ ENSURES_SCHEMA_REGISTRY,
20
+ type EnsuresSchemaSpec,
21
+ evidenceSchema,
22
+ LEGACY_PASSTHROUGH_SCHEMA_NAMES,
23
+ passthroughSchema,
24
+ taskTreeSchema,
25
+ } from '../ensures-schema-registry.js';
26
+
27
+ /** First Zod issue message for a failing parse (or `undefined` if it parsed). */
28
+ function firstIssueMessage(schema: typeof taskTreeSchema, value: unknown): string | undefined {
29
+ const result = schema.safeParse(value);
30
+ return result.success ? undefined : result.error.issues[0]?.message;
31
+ }
32
+
33
+ describe('ensures-schema-registry — registry DATA', () => {
34
+ it('registers the two strict schemas plus every legacy passthrough name', () => {
35
+ // T11762 ST-2 (R1): the two strict ports (`task_tree`, `evidence`) PLUS the
36
+ // legacy starter `.cantbook` schema names — registered with passthrough so
37
+ // the fail-closed flip does not regress the shipped playbooks.
38
+ expect([...ENSURES_SCHEMA_REGISTRY.keys()].sort()).toEqual(
39
+ ['task_tree', 'evidence', ...LEGACY_PASSTHROUGH_SCHEMA_NAMES].sort(),
40
+ );
41
+ });
42
+
43
+ it('each strict spec carries name, contextKey (= name by default), and a schema', () => {
44
+ for (const name of ['task_tree', 'evidence'] as const) {
45
+ const spec = ENSURES_SCHEMA_REGISTRY.get(name) as EnsuresSchemaSpec;
46
+ expect(spec).toBeDefined();
47
+ expect(spec.name).toBe(name);
48
+ expect(spec.contextKey).toBe(name);
49
+ expect(typeof spec.schema.safeParse).toBe('function');
50
+ }
51
+ });
52
+
53
+ it('every legacy passthrough name is registered with the passthrough schema', () => {
54
+ // R1: passthrough accepts ANY value (including `undefined`) so the historical
55
+ // "silently skipped" behavior is preserved EXACTLY.
56
+ for (const name of LEGACY_PASSTHROUGH_SCHEMA_NAMES) {
57
+ const spec = ENSURES_SCHEMA_REGISTRY.get(name) as EnsuresSchemaSpec;
58
+ expect(spec).toBeDefined();
59
+ expect(spec.name).toBe(name);
60
+ expect(spec.schema).toBe(passthroughSchema);
61
+ expect(spec.schema.safeParse(undefined).success).toBe(true);
62
+ expect(spec.schema.safeParse({ anything: 1 }).success).toBe(true);
63
+ expect(spec.schema.safeParse(42).success).toBe(true);
64
+ }
65
+ });
66
+ });
67
+
68
+ describe('ensures-schema-registry — task_tree (ports validateDecompositionTaskTree)', () => {
69
+ it('accepts a minimal valid task tree', () => {
70
+ expect(taskTreeSchema.safeParse([{ title: 'Build X', acceptance: ['does X'] }]).success).toBe(
71
+ true,
72
+ );
73
+ });
74
+
75
+ it('accepts a tree with optional id/parentId/depends populated', () => {
76
+ expect(
77
+ taskTreeSchema.safeParse([
78
+ { title: 'Root', acceptance: ['ac1'], id: 'T1' },
79
+ { title: 'Child', acceptance: ['ac2', 'ac3'], id: 'T2', parentId: 'T1', depends: ['T1'] },
80
+ ]).success,
81
+ ).toBe(true);
82
+ });
83
+
84
+ it('rejects a non-array (parity: "task_tree must be a non-empty array")', () => {
85
+ expect(firstIssueMessage(taskTreeSchema, 'not-an-array')).toBe(
86
+ 'task_tree must be a non-empty array',
87
+ );
88
+ });
89
+
90
+ it('rejects an EMPTY ARRAY (parity: empty-array message)', () => {
91
+ expect(firstIssueMessage(taskTreeSchema, [])).toBe(
92
+ 'task_tree is an empty array — decomposition produced no tasks',
93
+ );
94
+ });
95
+
96
+ it('rejects a MISSING TITLE (parity: "title must be a non-empty string")', () => {
97
+ expect(firstIssueMessage(taskTreeSchema, [{ acceptance: ['ac1'] }])).toBe(
98
+ 'title must be a non-empty string',
99
+ );
100
+ });
101
+
102
+ it('rejects a whitespace-only title (parity: same non-empty-title message)', () => {
103
+ expect(firstIssueMessage(taskTreeSchema, [{ title: ' ', acceptance: ['ac1'] }])).toBe(
104
+ 'title must be a non-empty string',
105
+ );
106
+ });
107
+
108
+ it('rejects an EMPTY ACCEPTANCE array (parity: non-empty-acceptance message)', () => {
109
+ expect(firstIssueMessage(taskTreeSchema, [{ title: 'X', acceptance: [] }])).toBe(
110
+ 'must have a non-empty acceptance array',
111
+ );
112
+ });
113
+
114
+ it('rejects an acceptance array with no non-empty strings (parity message)', () => {
115
+ expect(firstIssueMessage(taskTreeSchema, [{ title: 'X', acceptance: [' ', ''] }])).toBe(
116
+ 'acceptance array contains no non-empty strings',
117
+ );
118
+ });
119
+ });
120
+
121
+ describe('ensures-schema-registry — evidence (ports validateIvtrEvidenceOutput)', () => {
122
+ it('accepts a non-empty string', () => {
123
+ expect(evidenceSchema.safeParse('commit:abc123').success).toBe(true);
124
+ });
125
+
126
+ it('accepts a non-empty array', () => {
127
+ expect(evidenceSchema.safeParse(['atom']).success).toBe(true);
128
+ });
129
+
130
+ it('accepts an object with at least one key', () => {
131
+ expect(evidenceSchema.safeParse({ commit: 'abc' }).success).toBe(true);
132
+ });
133
+
134
+ it('rejects null/undefined (parity: must-be-present message)', () => {
135
+ expect(firstIssueMessage(evidenceSchema, null)).toBe(
136
+ 'evidence must be present (non-null, non-undefined)',
137
+ );
138
+ expect(firstIssueMessage(evidenceSchema, undefined)).toBe(
139
+ 'evidence must be present (non-null, non-undefined)',
140
+ );
141
+ });
142
+
143
+ it('rejects an empty / whitespace-only string (parity message)', () => {
144
+ expect(firstIssueMessage(evidenceSchema, '')).toBe('evidence string must not be empty');
145
+ expect(firstIssueMessage(evidenceSchema, ' ')).toBe('evidence string must not be empty');
146
+ });
147
+
148
+ it('rejects an empty array (parity message)', () => {
149
+ expect(firstIssueMessage(evidenceSchema, [])).toBe('evidence array must not be empty');
150
+ });
151
+
152
+ it('rejects an EMPTY OBJECT {} (parity: at-least-one-key message)', () => {
153
+ expect(firstIssueMessage(evidenceSchema, {})).toBe(
154
+ 'evidence object must have at least one key (got {})',
155
+ );
156
+ });
157
+
158
+ it('rejects NUMERIC evidence (parity: wrong-type message with typeof)', () => {
159
+ expect(firstIssueMessage(evidenceSchema, 42)).toBe(
160
+ 'evidence must be a string, array, or object (got number)',
161
+ );
162
+ });
163
+
164
+ it('rejects boolean evidence (parity: wrong-type message with typeof)', () => {
165
+ expect(firstIssueMessage(evidenceSchema, true)).toBe(
166
+ 'evidence must be a string, array, or object (got boolean)',
167
+ );
168
+ });
169
+ });
@@ -380,6 +380,9 @@ export type AdminOp =
380
380
  | 'admin.health'
381
381
  | 'admin.config.show'
382
382
  | 'admin.config.presets'
383
+ | 'admin.config.get'
384
+ | 'admin.config.list'
385
+ | 'admin.config.validate'
383
386
  | 'admin.stats'
384
387
  | 'admin.context'
385
388
  | 'admin.context.pull'
@@ -406,6 +409,7 @@ export type AdminOp =
406
409
  | 'admin.health.mutate'
407
410
  | 'admin.config.set'
408
411
  | 'admin.config.set-preset'
412
+ | 'admin.config.unset'
409
413
  | 'admin.backup.mutate'
410
414
  | 'admin.migrate'
411
415
  | 'admin.cleanup'
@@ -513,6 +517,109 @@ export interface AdminConfigPresetsResult {
513
517
  presets: AdminPresetDefinition[];
514
518
  }
515
519
 
520
+ // ---------------------------------------------------------------------------
521
+ // admin.config.get (T11917 — config-as-domain, ConfigManifest cascade)
522
+ // ---------------------------------------------------------------------------
523
+
524
+ /**
525
+ * Cascade slice selector for the config-as-domain query ops.
526
+ *
527
+ * - `'global'` — read ONLY `~/.cleo/config.json` (precedence 10)
528
+ * - `'project'` — read ONLY `<project>/.cleo/config.json` (precedence 20)
529
+ * - `'merged'` — read the deep-merge `{ ...global, ...project }` (default)
530
+ *
531
+ * Mirrors the `ResolveScope` of the `resolveCleoConfig` cascade resolver.
532
+ *
533
+ * @task T11917
534
+ */
535
+ export type AdminConfigScope = 'global' | 'project' | 'merged';
536
+
537
+ /** Parameters for `admin.config.get`. */
538
+ export interface AdminConfigGetParams {
539
+ /** Dot-notation config key to read a single value (required). */
540
+ key: string;
541
+ /** Cascade slice to read from. Defaults to `'merged'`. */
542
+ scope?: AdminConfigScope;
543
+ }
544
+
545
+ /**
546
+ * Result of `admin.config.get`.
547
+ *
548
+ * Returns the resolved value for `key`, or `null` when the key is absent in the
549
+ * selected cascade slice. `found` disambiguates a present `null` value from an
550
+ * absent key.
551
+ *
552
+ * @task T11917
553
+ */
554
+ export interface AdminConfigGetResult {
555
+ /** The dot-notation key that was resolved. */
556
+ key: string;
557
+ /** Cascade slice the value was resolved against. */
558
+ scope: AdminConfigScope;
559
+ /** Resolved value, or `null` when the key is absent. */
560
+ value: unknown;
561
+ /** `true` IFF the key resolved to a defined value in the cascade. */
562
+ found: boolean;
563
+ }
564
+
565
+ // ---------------------------------------------------------------------------
566
+ // admin.config.list (T11917 — full resolved cascade)
567
+ // ---------------------------------------------------------------------------
568
+
569
+ /** Parameters for `admin.config.list`. */
570
+ export interface AdminConfigListParams {
571
+ /** Cascade slice to project. Defaults to `'merged'`. */
572
+ scope?: AdminConfigScope;
573
+ }
574
+
575
+ /**
576
+ * Result of `admin.config.list`.
577
+ *
578
+ * Returns the full resolved config object for the selected cascade slice plus
579
+ * the flattened dot-notation key list for discovery.
580
+ *
581
+ * @task T11917
582
+ */
583
+ export interface AdminConfigListResult {
584
+ /** Cascade slice the config was resolved against. */
585
+ scope: AdminConfigScope;
586
+ /** Full resolved config object for the slice. */
587
+ config: Record<string, unknown>;
588
+ /** Flattened dot-notation keys present in the resolved config. */
589
+ keys: string[];
590
+ }
591
+
592
+ // ---------------------------------------------------------------------------
593
+ // admin.config.validate (T11917 — schema-validate the cascade entry)
594
+ // ---------------------------------------------------------------------------
595
+
596
+ /** Parameters for `admin.config.validate`. */
597
+ export interface AdminConfigValidateParams {
598
+ /**
599
+ * Scope to validate. Only the cascade-participating scopes are valid —
600
+ * `'merged'` is rejected because validation runs against a single file's
601
+ * manifest entry schema. Defaults to `'project'`.
602
+ */
603
+ scope?: 'global' | 'project';
604
+ }
605
+
606
+ /**
607
+ * Result of `admin.config.validate`.
608
+ *
609
+ * `ok` is `true` IFF the file passed its manifest entry's schema gate (or no
610
+ * schema was configured). `issues` carries human-readable rejection reasons.
611
+ *
612
+ * @task T11917
613
+ */
614
+ export interface AdminConfigValidateResult {
615
+ /** Scope that was validated. */
616
+ scope: 'global' | 'project';
617
+ /** `true` IFF every gate passed. */
618
+ ok: boolean;
619
+ /** Human-readable schema issues. Empty when `ok === true`. */
620
+ issues: string[];
621
+ }
622
+
516
623
  // ---------------------------------------------------------------------------
517
624
  // admin.stats
518
625
  // ---------------------------------------------------------------------------
@@ -1466,6 +1573,35 @@ export interface AdminConfigSetResult {
1466
1573
  value: unknown;
1467
1574
  }
1468
1575
 
1576
+ // ---------------------------------------------------------------------------
1577
+ // admin.config.unset (T11917 — remove a key from a scoped config file)
1578
+ // ---------------------------------------------------------------------------
1579
+
1580
+ /** Parameters for `admin.config.unset`. */
1581
+ export interface AdminConfigUnsetParams {
1582
+ /** Dot-notation config key to remove (required). */
1583
+ key: string;
1584
+ /** Remove from the global file instead of the project file. */
1585
+ global?: boolean;
1586
+ }
1587
+
1588
+ /**
1589
+ * Result of `admin.config.unset`.
1590
+ *
1591
+ * `removed` is `true` IFF the key existed and was deleted; `false` when the key
1592
+ * was already absent (idempotent no-op).
1593
+ *
1594
+ * @task T11917
1595
+ */
1596
+ export interface AdminConfigUnsetResult {
1597
+ /** Config key that was targeted. */
1598
+ key: string;
1599
+ /** Scope the key was removed from. */
1600
+ scope: 'project' | 'global';
1601
+ /** `true` IFF a value was actually deleted. */
1602
+ removed: boolean;
1603
+ }
1604
+
1469
1605
  // ---------------------------------------------------------------------------
1470
1606
  // admin.config.set-preset
1471
1607
  // ---------------------------------------------------------------------------
@@ -1996,6 +2132,13 @@ export type AdminOps =
1996
2132
  params: AdminConfigPresetsParams;
1997
2133
  result: AdminConfigPresetsResult;
1998
2134
  }
2135
+ | { op: 'admin.config.get'; params: AdminConfigGetParams; result: AdminConfigGetResult }
2136
+ | { op: 'admin.config.list'; params: AdminConfigListParams; result: AdminConfigListResult }
2137
+ | {
2138
+ op: 'admin.config.validate';
2139
+ params: AdminConfigValidateParams;
2140
+ result: AdminConfigValidateResult;
2141
+ }
1999
2142
  | { op: 'admin.stats'; params: AdminStatsParams; result: AdminStatsResult }
2000
2143
  | { op: 'admin.context'; params: AdminContextParams; result: AdminContextResult }
2001
2144
  | { op: 'admin.context.pull'; params: AdminContextPullParams; result: AdminContextPullResult }
@@ -2046,6 +2189,7 @@ export type AdminOps =
2046
2189
  params: AdminConfigSetPresetParams;
2047
2190
  result: AdminConfigSetPresetResult;
2048
2191
  }
2192
+ | { op: 'admin.config.unset'; params: AdminConfigUnsetParams; result: AdminConfigUnsetResult }
2049
2193
  | {
2050
2194
  op: 'admin.backup.mutate';
2051
2195
  params: AdminBackupMutateParams;
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Ensures-schema Zod registry — the cantbook `ensures.schema` name → Zod
3
+ * validator SSoT (T11762 ST-1 · Lane A).
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * The playbook runtime (`packages/playbooks/src/runtime.ts`) validates a node's
8
+ * post-merge `context` against a named shape declared as `ensures.schema:` in a
9
+ * `.cantbook`. Historically the runtime HARDCODED two bespoke validators
10
+ * (`validateDecompositionTaskTree` / `validateIvtrEvidenceOutput`) in an
11
+ * `if (schema === 'task_tree') … else if (=== 'evidence')` block, and silently
12
+ * SKIPPED any other schema name. T11762 replaces that hardcode with a
13
+ * registry-driven lookup so:
14
+ *
15
+ * - `ensures.schema` names resolve to a registered Zod validator (AC1).
16
+ * - the runtime no longer hardcodes `'task_tree'` / `'evidence'` (AC2).
17
+ * - an unknown schema name FAILS CLOSED instead of silently passing.
18
+ *
19
+ * This module holds ONLY the Gate-10 (`lint-no-runtime-in-contracts`)-ALLOWED
20
+ * kinds: the Zod schema **const values**, the {@link EnsuresSchemaSpec} **type**,
21
+ * and the registry **data** (a `ReadonlyMap` value). The bodied accessors
22
+ * (`getEnsuresSchema` / `listEnsuresSchemaNames` / `defineEnsuresSchema`) live in
23
+ * `@cleocode/core` (`packages/core/src/dispatch/contracts/ensures-schema.ts`,
24
+ * ST-1b) because a `Map.get` helper is a net-new runtime function that the
25
+ * contracts-purity gate forbids in this leaf package.
26
+ *
27
+ * ## Message parity (behavior preservation)
28
+ *
29
+ * The Zod schemas are crafted so that — for every input the deleted bespoke
30
+ * validators rejected — the FIRST Zod issue message equals the SUFFIX of the
31
+ * original human-readable violation string (the part after the
32
+ * `ensures.schema[<name>] on <nodeId>: ` prefix). ST-2's runtime wrapper
33
+ * re-applies that prefix, so the end-to-end violation strings are unchanged. The
34
+ * ST-1 parity test asserts on these issue messages directly.
35
+ *
36
+ * ## Genkit-forward naming
37
+ *
38
+ * The registry surface is deliberately Genkit-shaped so a future Genkit
39
+ * `ai.defineSchema(name, zodSchema)` bridge (T11768) is a thin adapter over this
40
+ * SSoT, never a competing source.
41
+ *
42
+ * @packageDocumentation
43
+ * @module @cleocode/contracts/operations/ensures-schema-registry
44
+ *
45
+ * @epic T11762 — E-OUTPUT-SCHEMA-ENFORCEMENT
46
+ * @task T11900 — ST-1: ensures-schema Zod registry DATA + schemas
47
+ * @see validateDecompositionTaskTree — the ported bespoke task_tree validator
48
+ * @see validateIvtrEvidenceOutput — the ported bespoke evidence validator
49
+ */
50
+
51
+ import { type ZodType, z } from 'zod';
52
+
53
+ /**
54
+ * One registered `ensures.schema` entry: the Zod validator plus which playbook
55
+ * `context` key holds the value to validate.
56
+ *
57
+ * @public
58
+ */
59
+ export interface EnsuresSchemaSpec {
60
+ /**
61
+ * The schema NAME used in `ensures.schema:` (e.g. `'task_tree'`, `'evidence'`).
62
+ */
63
+ readonly name: string;
64
+ /**
65
+ * The playbook `context` key whose value is validated. Defaults to {@link name}
66
+ * (matching today's `context['task_tree']` / `context['evidence']` convention).
67
+ */
68
+ readonly contextKey: string;
69
+ /**
70
+ * The Zod validator applied to `context[contextKey]`.
71
+ */
72
+ readonly schema: ZodType;
73
+ }
74
+
75
+ // ─── task_tree — ports validateDecompositionTaskTree (runtime.ts:794-855) ──────
76
+
77
+ /**
78
+ * Zod schema for a single `task_tree` entry.
79
+ *
80
+ * Parity rules ported from `validateDecompositionTaskTree`:
81
+ * - `title` must be a non-empty string (after trim).
82
+ * - `acceptance` must be a non-empty array containing at least one non-empty
83
+ * (after-trim) string.
84
+ * - `id` / `parentId` / `depends` are optional and not hard-validated (the
85
+ * bespoke validator was intentionally lenient on these).
86
+ *
87
+ * @remarks
88
+ * The `title` check is split so the message echoes the bespoke
89
+ * `entry[i].title must be a non-empty string` regardless of whether the field is
90
+ * absent, the wrong type, or whitespace-only.
91
+ */
92
+ export const taskTreeEntrySchema: ZodType = z.object({
93
+ title: z
94
+ .string({ message: 'title must be a non-empty string' })
95
+ .refine((s) => s.trim().length > 0, { message: 'title must be a non-empty string' }),
96
+ acceptance: z
97
+ .array(z.unknown(), { message: 'must have a non-empty acceptance array' })
98
+ .min(1, { message: 'must have a non-empty acceptance array' })
99
+ .refine((arr) => arr.some((s) => typeof s === 'string' && s.trim().length > 0), {
100
+ message: 'acceptance array contains no non-empty strings',
101
+ }),
102
+ id: z.string().optional(),
103
+ parentId: z.string().optional(),
104
+ depends: z.array(z.string()).optional(),
105
+ });
106
+
107
+ /**
108
+ * Zod schema for the `task_tree` context value emitted by a RCASD decomposition
109
+ * node — a non-empty array of {@link taskTreeEntrySchema} entries.
110
+ *
111
+ * Parity: an empty array is rejected with `task_tree is an empty array …`; a
112
+ * non-array is rejected by Zod's base array type error.
113
+ *
114
+ * @public
115
+ */
116
+ export const taskTreeSchema: ZodType = z
117
+ .array(taskTreeEntrySchema, { message: 'task_tree must be a non-empty array' })
118
+ .min(1, { message: 'task_tree is an empty array — decomposition produced no tasks' });
119
+
120
+ // ─── evidence — ports validateIvtrEvidenceOutput (runtime.ts:873-902) ──────────
121
+
122
+ /**
123
+ * Zod schema for the `evidence` context value emitted by an IVTR validation
124
+ * node.
125
+ *
126
+ * Parity rules ported from `validateIvtrEvidenceOutput`. Valid evidence is one
127
+ * of:
128
+ * - a non-empty (after-trim) string,
129
+ * - a non-empty array,
130
+ * - an object with at least one key.
131
+ *
132
+ * `null` / `undefined`, the empty string, the empty array, the empty object, and
133
+ * any other primitive (number, boolean) are rejected — matching the bespoke
134
+ * validator exactly.
135
+ *
136
+ * @remarks
137
+ * A `z.union` of the three valid shapes would reject the SAME set of inputs, but
138
+ * collapses every rejection to a generic `"Invalid input"` union error. To
139
+ * preserve the bespoke validator's branch-specific messages (`evidence must be
140
+ * present …`, `evidence string must not be empty`, `evidence array must not be
141
+ * empty`, `evidence object must have at least one key (got {})`, `evidence must
142
+ * be a string, array, or object (got <type>)`), this schema replicates the
143
+ * original branch order in a single `superRefine`. The `superRefine` callback is
144
+ * an inline argument — NOT an exported bodied function/arrow — so it does not
145
+ * trip the Gate-10 contracts-purity lint.
146
+ *
147
+ * @public
148
+ */
149
+ export const evidenceSchema: ZodType = z.unknown().superRefine((value, ctx) => {
150
+ if (value === null || value === undefined) {
151
+ ctx.addIssue({
152
+ code: 'custom',
153
+ message: 'evidence must be present (non-null, non-undefined)',
154
+ });
155
+ return;
156
+ }
157
+ if (typeof value === 'string') {
158
+ if (value.trim().length === 0) {
159
+ ctx.addIssue({ code: 'custom', message: 'evidence string must not be empty' });
160
+ }
161
+ return;
162
+ }
163
+ if (Array.isArray(value)) {
164
+ if (value.length === 0) {
165
+ ctx.addIssue({ code: 'custom', message: 'evidence array must not be empty' });
166
+ }
167
+ return;
168
+ }
169
+ if (typeof value === 'object') {
170
+ if (Object.keys(value).length === 0) {
171
+ ctx.addIssue({
172
+ code: 'custom',
173
+ message: 'evidence object must have at least one key (got {})',
174
+ });
175
+ }
176
+ return;
177
+ }
178
+ // Numbers, booleans, symbols, bigints, functions — not valid evidence shapes.
179
+ ctx.addIssue({
180
+ code: 'custom',
181
+ message: `evidence must be a string, array, or object (got ${typeof value})`,
182
+ });
183
+ });
184
+
185
+ // ─── passthrough — legacy starter `ensures.schema` names (R1 fail-closed) ──────
186
+
187
+ /**
188
+ * Passthrough validator that accepts ANY value (including `undefined`).
189
+ *
190
+ * Used to register the legacy `ensures.schema` names already declared in the
191
+ * shipped starter `.cantbook` files ({@link LEGACY_PASSTHROUGH_SCHEMA_NAMES}).
192
+ *
193
+ * @remarks
194
+ * Before T11762, the runtime hardcoded ONLY `task_tree` / `evidence` and
195
+ * SILENTLY SKIPPED every other `ensures.schema` name. ST-2 flips that to
196
+ * fail-closed: an UNREGISTERED name is now a contract violation. To avoid
197
+ * regressing the in-repo starter playbooks (`release` / `rcasd` / `ivtr`),
198
+ * which declare a dozen descriptive schema names that were never actually
199
+ * shape-checked, each such name is registered here with this passthrough
200
+ * schema — preserving their historical "always pass" behavior EXACTLY while
201
+ * still failing-closed for genuinely-unknown names (Risk R1). Tightening any of
202
+ * these into a real shape is a follow-up; the name is the SSoT anchor.
203
+ *
204
+ * @public
205
+ */
206
+ export const passthroughSchema: ZodType = z.unknown();
207
+
208
+ /**
209
+ * The legacy `ensures.schema` names declared in the shipped starter
210
+ * `.cantbook` files that were SILENTLY SKIPPED pre-T11762. Registered with
211
+ * {@link passthroughSchema} so the fail-closed flip (ST-2) does not regress
212
+ * them. Kept as exported DATA so the parity test can assert the exact set.
213
+ *
214
+ * @public
215
+ */
216
+ export const LEGACY_PASSTHROUGH_SCHEMA_NAMES: readonly string[] = [
217
+ // release.cantbook
218
+ 'version_bump_report',
219
+ 'changelog_entry',
220
+ 'publish_report',
221
+ // rcasd.cantbook
222
+ 'research_summary',
223
+ 'consensus_decision',
224
+ 'architecture_plan',
225
+ 'specification_document',
226
+ // ivtr.cantbook
227
+ 'implementation_diff',
228
+ 'validation_report',
229
+ 'audit_report',
230
+ 'test_report',
231
+ 'release_confirmation',
232
+ ];
233
+
234
+ /**
235
+ * The ensures-schema registry DATA — a `ReadonlyMap` from `ensures.schema` name
236
+ * to its {@link EnsuresSchemaSpec}.
237
+ *
238
+ * This is a VALUE (data), not a bodied function, so it is Gate-10-safe in the
239
+ * `@cleocode/contracts` leaf package. The bodied accessor layer in
240
+ * `@cleocode/core` (ST-1b) reads this map.
241
+ *
242
+ * Two strict entries (`task_tree`, `evidence`) port the deleted bespoke
243
+ * validators; the remaining entries are the legacy starter names
244
+ * ({@link LEGACY_PASSTHROUGH_SCHEMA_NAMES}) registered with
245
+ * {@link passthroughSchema} so the ST-2 fail-closed flip does not regress the
246
+ * shipped playbooks (Risk R1).
247
+ *
248
+ * @public
249
+ */
250
+ export const ENSURES_SCHEMA_REGISTRY: ReadonlyMap<string, EnsuresSchemaSpec> = new Map<
251
+ string,
252
+ EnsuresSchemaSpec
253
+ >([
254
+ ['task_tree', { name: 'task_tree', contextKey: 'task_tree', schema: taskTreeSchema }],
255
+ ['evidence', { name: 'evidence', contextKey: 'evidence', schema: evidenceSchema }],
256
+ ...LEGACY_PASSTHROUGH_SCHEMA_NAMES.map((name): [string, EnsuresSchemaSpec] => [
257
+ name,
258
+ { name, contextKey: name, schema: passthroughSchema },
259
+ ]),
260
+ ]);