@tech-leads-club/harness-toolkit 0.5.1 → 0.6.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.
package/schema.json ADDED
@@ -0,0 +1,545 @@
1
+ {
2
+ "additionalProperties": false,
3
+ "type": "object",
4
+ "properties": {
5
+ "$schema": {
6
+ "type": "string"
7
+ },
8
+ "version": {
9
+ "const": 1,
10
+ "type": "number"
11
+ },
12
+ "mode": {
13
+ "enum": [
14
+ "focus",
15
+ "paired",
16
+ "solo"
17
+ ],
18
+ "type": "string"
19
+ },
20
+ "projectName": {
21
+ "type": "string"
22
+ },
23
+ "codePaths": {
24
+ "type": "array",
25
+ "items": {
26
+ "type": "string"
27
+ }
28
+ },
29
+ "grind": {
30
+ "$ref": "#/definitions/Partial%3C%7Benabled%3Aboolean%3BmaxLoops%3Anumber%3BlintCommand%3Astring%5B%5D%7Cnull%3BtestCommand%3Astring%5B%5D%7Cnull%3BappendFiles%3Aimport(%22.%2Fsrc%2Fcore%2Fgate%2Fgate.types%22%2C%7Bwith%3A%7B%22resolution-mode%22%3A%22import%22%7D%7D).AppendFilesMode%3B%7D%3E"
31
+ },
32
+ "shipGate": {
33
+ "$ref": "#/definitions/Partial%3C%7Benabled%3Aboolean%3BruntimePathPrefixes%3Astring%5B%5D%3BruntimePathExcludes%3Astring%5B%5D%3BevidenceDir%3Astring%7Cnull%3BevidenceMaxAgeHours%3Anumber%3BemptyDiffAntiShip%3Aboolean%3BclaimWindowMinutes%3Anumber%3B%7D%3E"
34
+ },
35
+ "subagents": {
36
+ "$ref": "#/definitions/Partial%3C%7BenforceAllowlist%3Aboolean%3BrequireModel%3Aboolean%3BallowedModels%3Aimport(%22.%2Fsrc%2Fcore%2Fpolicy%2Fpolicy.types%22%2C%7Bwith%3A%7B%22resolution-mode%22%3A%22import%22%7D%7D).ProviderScoped%3Cstring%3E%3BblockedPatterns%3Aimport(%22.%2Fsrc%2Fcore%2Fpolicy%2Fpolicy.types%22%2C%7Bwith%3A%7B%22resolution-mode%22%3A%22import%22%7D%7D).ProviderScoped%3Cstring%3E%3BminEffort%3A%22low%22%7C%22medium%22%7C%22high%22%7C%22xhigh%22%7C%22max%22%7Cnull%3BblockParentFast%3Aboolean%3BblockMode%3A%22deny%22%7C%22ask%22%3BreadOnlyTypes%3Astring%5B%5D%3B%7D%3E"
37
+ },
38
+ "docs": {
39
+ "$ref": "#/definitions/Partial%3C%7Bcommand%3Astring%5B%5D%7Cnull%3Bseverity%3A%22deny%22%7C%22warn%22%3B%7D%3E"
40
+ },
41
+ "observe": {
42
+ "description": "why: observation runs a rail's checker while the rail is not enforcing, so the harness can tell \"the model\nalready does this\" from \"the rule works\". Opt-in and default empty: it costs a diff scan per turn and answers a\nquestion only an operator who is asking it needs answered.",
43
+ "type": "object",
44
+ "properties": {
45
+ "enabled": {
46
+ "type": "boolean"
47
+ },
48
+ "rails": {
49
+ "description": "Rails to observe by name. An unknown name is inert rather than an error — a rail may not exist yet.",
50
+ "type": "array",
51
+ "items": {
52
+ "type": "string"
53
+ }
54
+ }
55
+ },
56
+ "additionalProperties": false,
57
+ "required": [
58
+ "enabled",
59
+ "rails"
60
+ ]
61
+ },
62
+ "comments": {
63
+ "$ref": "#/definitions/Partial%3C%7Benabled%3Aboolean%3BonViolation%3A%22followup%22%7C%22off%22%3Bmode%3Aimport(%22.%2Fsrc%2Fcore%2Fpolicy%2Fpolicy.types%22%2C%7Bwith%3A%7B%22resolution-mode%22%3A%22import%22%7D%7D).CommentMode%3B%7D%3E"
64
+ },
65
+ "supplyChain": {
66
+ "type": "object",
67
+ "properties": {
68
+ "enabled": {
69
+ "type": "boolean"
70
+ }
71
+ },
72
+ "additionalProperties": false,
73
+ "required": [
74
+ "enabled"
75
+ ]
76
+ },
77
+ "duplication": {
78
+ "type": "object",
79
+ "properties": {
80
+ "enabled": {
81
+ "type": "boolean"
82
+ },
83
+ "minRun": {
84
+ "description": "why: the window is the only knob, because it is the only one calibration moved. Six is where matches stopped being punctuation.",
85
+ "type": "number"
86
+ }
87
+ },
88
+ "additionalProperties": false,
89
+ "required": [
90
+ "enabled",
91
+ "minRun"
92
+ ]
93
+ },
94
+ "obs": {
95
+ "$ref": "#/definitions/Partial%3C%7BglobalSpool%3Aboolean%3BincludePayloads%3Aboolean%3BmaxAttrChars%3Anumber%3BsessionCostAlertUsd%3Anumber%7Cnull%3BretentionDays%3Anumber%3B%7D%3E"
96
+ },
97
+ "untrustedContent": {
98
+ "$ref": "#/definitions/Partial%3C%7Benabled%3Aboolean%3Bmode%3Aimport(%22.%2Fsrc%2Fcore%2Funtrusted%2Funtrusted.types%22%2C%7Bwith%3A%7B%22resolution-mode%22%3A%22import%22%7D%7D).UntrustedMode%3BextraTools%3Astring%5B%5D%3BextraCommandPatterns%3Astring%5B%5D%3B%7D%3E"
99
+ },
100
+ "planGate": {
101
+ "$ref": "#/definitions/Partial%3C%7Benabled%3Aboolean%3BwindowMinutes%3Anumber%3B%7D%3E"
102
+ },
103
+ "rules": {
104
+ "$ref": "#/definitions/Partial%3C%7Benabled%3Aboolean%3B%7D%3E"
105
+ },
106
+ "shell": {
107
+ "$ref": "#/definitions/Partial%3C%7BcatastrophicAsk%3Aboolean%3BstallDetection%3Aboolean%3BstallRepeatThreshold%3Anumber%3B%7D%3E"
108
+ },
109
+ "intelligence": {
110
+ "additionalProperties": false,
111
+ "type": "object",
112
+ "properties": {
113
+ "gapFeedback": {
114
+ "type": "boolean"
115
+ },
116
+ "failureClassification": {
117
+ "type": "boolean"
118
+ },
119
+ "progressiveHandoff": {
120
+ "type": "boolean"
121
+ },
122
+ "progressiveContext": {
123
+ "type": "boolean"
124
+ },
125
+ "autopilot": {
126
+ "type": "boolean"
127
+ },
128
+ "idleTurnGate": {
129
+ "type": "boolean"
130
+ },
131
+ "budgetContinue": {
132
+ "type": "boolean"
133
+ },
134
+ "budgetContinueAfterLoops": {
135
+ "type": "number"
136
+ },
137
+ "lessons": {
138
+ "$ref": "#/definitions/Partial%3Cimport(%22.%2Fsrc%2Fcore%2Fpolicy%2Fpolicy.types%22%2C%7Bwith%3A%7B%22resolution-mode%22%3A%22import%22%7D%7D).LessonsPolicyConfig%3E"
139
+ }
140
+ }
141
+ },
142
+ "mcpPrime": {
143
+ "type": "array",
144
+ "items": {
145
+ "type": "string"
146
+ }
147
+ },
148
+ "bootstrapExtra": {
149
+ "type": "array",
150
+ "items": {
151
+ "type": "string"
152
+ }
153
+ }
154
+ },
155
+ "definitions": {
156
+ "AppendFilesMode": {
157
+ "enum": [
158
+ "always",
159
+ "auto",
160
+ "never"
161
+ ],
162
+ "type": "string"
163
+ },
164
+ "import(\"./src/core/policy/policy.types\",{with:{\"resolution-mode\":\"import\"}}).ProviderScoped<string>": {
165
+ "anyOf": [
166
+ {
167
+ "type": "array",
168
+ "items": {
169
+ "type": "string"
170
+ }
171
+ },
172
+ {
173
+ "$ref": "#/definitions/Record%3Cstring%2Cstring%5B%5D%3E"
174
+ }
175
+ ]
176
+ },
177
+ "Record<string,string[]>": {
178
+ "type": "object",
179
+ "additionalProperties": false
180
+ },
181
+ "CommentMode": {
182
+ "description": "`declared` requires a reason marker, `strict` accepts no added comment at all, and `resolvable` is `declared`\nplus the question neither can ask: does the comment mean anything to a reader who was not in the session that\nwrote it ([/decisions/ad-070.md](/decisions/ad-070.md))?",
183
+ "enum": [
184
+ "declared",
185
+ "resolvable",
186
+ "strict"
187
+ ],
188
+ "type": "string"
189
+ },
190
+ "UntrustedMode": {
191
+ "description": "`frame` states once per turn that outside content is data. `enforce` adds the question framing cannot ask —\ndid this command come from that content — and answers it verbatim\n([/decisions/ad-077.md](/decisions/ad-077.md)).",
192
+ "enum": [
193
+ "enforce",
194
+ "frame"
195
+ ],
196
+ "type": "string"
197
+ },
198
+ "LessonsSyncMode": {
199
+ "description": "How the durable provider view is decided. `auto` asks the host: a provider that does not deliver context from\nits session-start hook has no other route, so the view is written there and not where the hook works\n([/decisions/ad-050.md](/decisions/ad-050.md)).",
200
+ "enum": [
201
+ "always",
202
+ "auto",
203
+ "never"
204
+ ],
205
+ "type": "string"
206
+ },
207
+ "Partial<{enabled:boolean;maxLoops:number;lintCommand:string[]|null;testCommand:string[]|null;appendFiles:import(\"./src/core/gate/gate.types\",{with:{\"resolution-mode\":\"import\"}}).AppendFilesMode;}>": {
208
+ "type": "object",
209
+ "properties": {
210
+ "enabled": {
211
+ "type": "boolean"
212
+ },
213
+ "maxLoops": {
214
+ "type": "number"
215
+ },
216
+ "lintCommand": {
217
+ "anyOf": [
218
+ {
219
+ "type": "array",
220
+ "items": {
221
+ "type": "string"
222
+ }
223
+ },
224
+ {
225
+ "type": "null"
226
+ }
227
+ ]
228
+ },
229
+ "testCommand": {
230
+ "anyOf": [
231
+ {
232
+ "type": "array",
233
+ "items": {
234
+ "type": "string"
235
+ }
236
+ },
237
+ {
238
+ "type": "null"
239
+ }
240
+ ]
241
+ },
242
+ "appendFiles": {
243
+ "enum": [
244
+ "always",
245
+ "auto",
246
+ "never"
247
+ ],
248
+ "type": "string"
249
+ }
250
+ },
251
+ "additionalProperties": false
252
+ },
253
+ "Partial<{enabled:boolean;runtimePathPrefixes:string[];runtimePathExcludes:string[];evidenceDir:string|null;evidenceMaxAgeHours:number;emptyDiffAntiShip:boolean;claimWindowMinutes:number;}>": {
254
+ "type": "object",
255
+ "properties": {
256
+ "enabled": {
257
+ "type": "boolean"
258
+ },
259
+ "runtimePathPrefixes": {
260
+ "type": "array",
261
+ "items": {
262
+ "type": "string"
263
+ }
264
+ },
265
+ "runtimePathExcludes": {
266
+ "type": "array",
267
+ "items": {
268
+ "type": "string"
269
+ }
270
+ },
271
+ "evidenceDir": {
272
+ "type": [
273
+ "null",
274
+ "string"
275
+ ]
276
+ },
277
+ "evidenceMaxAgeHours": {
278
+ "type": "number"
279
+ },
280
+ "emptyDiffAntiShip": {
281
+ "type": "boolean"
282
+ },
283
+ "claimWindowMinutes": {
284
+ "type": "number"
285
+ }
286
+ },
287
+ "additionalProperties": false
288
+ },
289
+ "Partial<{enforceAllowlist:boolean;requireModel:boolean;allowedModels:import(\"./src/core/policy/policy.types\",{with:{\"resolution-mode\":\"import\"}}).ProviderScoped<string>;blockedPatterns:import(\"./src/core/policy/policy.types\",{with:{\"resolution-mode\":\"import\"}}).ProviderScoped<string>;minEffort:\"low\"|\"medium\"|\"high\"|\"xhigh\"|\"max\"|null;blockParentFast:boolean;blockMode:\"deny\"|\"ask\";readOnlyTypes:string[];}>": {
290
+ "type": "object",
291
+ "properties": {
292
+ "enforceAllowlist": {
293
+ "type": "boolean"
294
+ },
295
+ "requireModel": {
296
+ "type": "boolean"
297
+ },
298
+ "allowedModels": {
299
+ "anyOf": [
300
+ {
301
+ "type": "array",
302
+ "items": {
303
+ "type": "string"
304
+ }
305
+ },
306
+ {
307
+ "$ref": "#/definitions/Record%3Cstring%2Cstring%5B%5D%3E"
308
+ }
309
+ ]
310
+ },
311
+ "blockedPatterns": {
312
+ "anyOf": [
313
+ {
314
+ "type": "array",
315
+ "items": {
316
+ "type": "string"
317
+ }
318
+ },
319
+ {
320
+ "$ref": "#/definitions/Record%3Cstring%2Cstring%5B%5D%3E"
321
+ }
322
+ ]
323
+ },
324
+ "minEffort": {
325
+ "anyOf": [
326
+ {
327
+ "enum": [
328
+ "high",
329
+ "low",
330
+ "max",
331
+ "medium",
332
+ "xhigh"
333
+ ],
334
+ "type": "string"
335
+ },
336
+ {
337
+ "type": "null"
338
+ }
339
+ ]
340
+ },
341
+ "blockParentFast": {
342
+ "type": "boolean"
343
+ },
344
+ "blockMode": {
345
+ "enum": [
346
+ "ask",
347
+ "deny"
348
+ ],
349
+ "type": "string"
350
+ },
351
+ "readOnlyTypes": {
352
+ "type": "array",
353
+ "items": {
354
+ "type": "string"
355
+ }
356
+ }
357
+ },
358
+ "additionalProperties": false
359
+ },
360
+ "Partial<{command:string[]|null;severity:\"deny\"|\"warn\";}>": {
361
+ "type": "object",
362
+ "properties": {
363
+ "command": {
364
+ "description": "Null means the gate does not exist for this project.",
365
+ "anyOf": [
366
+ {
367
+ "type": "array",
368
+ "items": {
369
+ "type": "string"
370
+ }
371
+ },
372
+ {
373
+ "type": "null"
374
+ }
375
+ ]
376
+ },
377
+ "severity": {
378
+ "enum": [
379
+ "deny",
380
+ "warn"
381
+ ],
382
+ "type": "string"
383
+ }
384
+ },
385
+ "additionalProperties": false
386
+ },
387
+ "Partial<{enabled:boolean;onViolation:\"followup\"|\"off\";mode:import(\"./src/core/policy/policy.types\",{with:{\"resolution-mode\":\"import\"}}).CommentMode;}>": {
388
+ "type": "object",
389
+ "properties": {
390
+ "enabled": {
391
+ "type": "boolean"
392
+ },
393
+ "onViolation": {
394
+ "enum": [
395
+ "followup",
396
+ "off"
397
+ ],
398
+ "type": "string"
399
+ },
400
+ "mode": {
401
+ "description": "`declared` requires a reason marker, `strict` accepts no added comment at all, and `resolvable` is `declared`\nplus the question neither can ask: does the comment mean anything to a reader who was not in the session that\nwrote it ([/decisions/ad-070.md](/decisions/ad-070.md))?",
402
+ "enum": [
403
+ "declared",
404
+ "resolvable",
405
+ "strict"
406
+ ],
407
+ "type": "string"
408
+ }
409
+ },
410
+ "additionalProperties": false
411
+ },
412
+ "Partial<{globalSpool:boolean;includePayloads:boolean;maxAttrChars:number;sessionCostAlertUsd:number|null;retentionDays:number;}>": {
413
+ "type": "object",
414
+ "properties": {
415
+ "globalSpool": {
416
+ "type": "boolean"
417
+ },
418
+ "includePayloads": {
419
+ "type": "boolean"
420
+ },
421
+ "maxAttrChars": {
422
+ "type": "number"
423
+ },
424
+ "sessionCostAlertUsd": {
425
+ "type": [
426
+ "null",
427
+ "number"
428
+ ]
429
+ },
430
+ "retentionDays": {
431
+ "type": "number"
432
+ }
433
+ },
434
+ "additionalProperties": false
435
+ },
436
+ "Partial<{enabled:boolean;mode:import(\"./src/core/untrusted/untrusted.types\",{with:{\"resolution-mode\":\"import\"}}).UntrustedMode;extraTools:string[];extraCommandPatterns:string[];}>": {
437
+ "type": "object",
438
+ "properties": {
439
+ "enabled": {
440
+ "type": "boolean"
441
+ },
442
+ "mode": {
443
+ "description": "`frame` states once per turn that outside content is data. `enforce` adds the question framing cannot ask —\ndid this command come from that content ([/decisions/ad-077.md](/decisions/ad-077.md))?",
444
+ "enum": [
445
+ "enforce",
446
+ "frame"
447
+ ],
448
+ "type": "string"
449
+ },
450
+ "extraTools": {
451
+ "type": "array",
452
+ "items": {
453
+ "type": "string"
454
+ }
455
+ },
456
+ "extraCommandPatterns": {
457
+ "type": "array",
458
+ "items": {
459
+ "type": "string"
460
+ }
461
+ }
462
+ },
463
+ "additionalProperties": false
464
+ },
465
+ "Partial<{enabled:boolean;windowMinutes:number;}>": {
466
+ "type": "object",
467
+ "properties": {
468
+ "enabled": {
469
+ "type": "boolean"
470
+ },
471
+ "windowMinutes": {
472
+ "type": "number"
473
+ }
474
+ },
475
+ "additionalProperties": false
476
+ },
477
+ "Partial<{enabled:boolean;}>": {
478
+ "type": "object",
479
+ "properties": {
480
+ "enabled": {
481
+ "type": "boolean"
482
+ }
483
+ },
484
+ "additionalProperties": false
485
+ },
486
+ "Partial<{catastrophicAsk:boolean;stallDetection:boolean;stallRepeatThreshold:number;}>": {
487
+ "type": "object",
488
+ "properties": {
489
+ "catastrophicAsk": {
490
+ "type": "boolean"
491
+ },
492
+ "stallDetection": {
493
+ "type": "boolean"
494
+ },
495
+ "stallRepeatThreshold": {
496
+ "type": "number"
497
+ }
498
+ },
499
+ "additionalProperties": false
500
+ },
501
+ "Partial<import(\"./src/core/policy/policy.types\",{with:{\"resolution-mode\":\"import\"}}).LessonsPolicyConfig>": {
502
+ "type": "object",
503
+ "properties": {
504
+ "enabled": {
505
+ "type": "boolean"
506
+ },
507
+ "maxInjectSession": {
508
+ "type": "number"
509
+ },
510
+ "maxInjectRetry": {
511
+ "type": "number"
512
+ },
513
+ "maxCharsSession": {
514
+ "type": "number"
515
+ },
516
+ "maxCharsRetry": {
517
+ "type": "number"
518
+ },
519
+ "promoteHitCount": {
520
+ "type": "number"
521
+ },
522
+ "decayLambda": {
523
+ "type": "number"
524
+ },
525
+ "projectBoost": {
526
+ "type": "number"
527
+ },
528
+ "syncRulesFile": {
529
+ "description": "How the durable provider view is decided. `auto` asks the host: a provider that does not deliver context from\nits session-start hook has no other route, so the view is written there and not where the hook works\n([/decisions/ad-050.md](/decisions/ad-050.md)).",
530
+ "enum": [
531
+ "always",
532
+ "auto",
533
+ "never"
534
+ ],
535
+ "type": "string"
536
+ },
537
+ "gardenOnSessionEnd": {
538
+ "type": "boolean"
539
+ }
540
+ },
541
+ "additionalProperties": false
542
+ }
543
+ },
544
+ "$schema": "http://json-schema.org/draft-07/schema#"
545
+ }
@@ -27,8 +27,34 @@ export function loadCatalog(home = runtimeHome()): CapabilityCatalog | null {
27
27
  return raw;
28
28
  }
29
29
 
30
+ export type ConfigReadStatus =
31
+ | { status: "absent" }
32
+ | { status: "parsed"; value: Record<string, unknown> }
33
+ | { status: "malformed"; error: string };
34
+
35
+ /**
36
+ * why: `readJson` already reads this file but collapses "absent" and "malformed" to the same `null`
37
+ * — the exact silent-corruption gap `doctor` needs to close, so this is a second, tagged reader.
38
+ */
39
+ export function readProjectPolicyStatus(projectDir: string): ConfigReadStatus {
40
+ const path = projectConfigPath(projectDir);
41
+ if (!existsSync(path)) {
42
+ return { status: "absent" };
43
+ }
44
+ try {
45
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
46
+ // why: a JSON Schema meta-key, not a Policy field — see the identical strip in policy.loader.ts,
47
+ // this is the second, independent read of the same file.
48
+ delete parsed.$schema;
49
+ return { status: "parsed", value: parsed };
50
+ } catch (error) {
51
+ return { status: "malformed", error: error instanceof Error ? error.message : String(error) };
52
+ }
53
+ }
54
+
30
55
  export function readProjectPolicyRaw(projectDir: string): Record<string, unknown> | null {
31
- return readJson<Record<string, unknown>>(projectConfigPath(projectDir));
56
+ const result = readProjectPolicyStatus(projectDir);
57
+ return result.status === "parsed" ? result.value : null;
32
58
  }
33
59
 
34
60
  function runtimeSeenPath(projectDir: string): string {
@@ -62,14 +62,9 @@ export function isCommentLine(text: string, file = ""): boolean {
62
62
  }
63
63
 
64
64
  /**
65
- * hazard: the rail's own scan target used to be `filterCodeTargets`, a nine-extension regex shared with grind
66
- * and duplication. The syntax catalog already knows 40+ languages — Terraform, SQL, YAML, Ruby, Java among
67
- * them — so a project whose changed files never carried one of the nine extensions had `mode: "strict"` in its
68
- * config and zero comments ever scanned. This filters by the same source the scanner itself trusts.
69
- *
70
- * invariant: takes no `codePaths` — that would need `policy.loader.ts`, which imports `policy.defaults.ts`,
71
- * which imports `duplication.service.ts`, which imports this module for `matchesSyntax`. A caller scopes by
72
- * `codePaths` itself first; this filters what is left by syntax alone.
65
+ * invariant: no `codePaths` param. Taking one needs `policy.loader.ts` → `policy.defaults.ts` →
66
+ * `duplication.service.ts`, which imports this module back for `matchesSyntax` — an import cycle. The
67
+ * caller scopes by `codePaths` first; this filters what's left by syntax alone.
73
68
  */
74
69
  export function filterCommentTargets(relativePaths: string[]): string[] {
75
70
  return relativePaths.filter((path) => syntaxFor(path) !== null);
@@ -16,6 +16,7 @@ import {
16
16
  import {
17
17
  loadCatalog,
18
18
  readProjectPolicyRaw,
19
+ readProjectPolicyStatus,
19
20
  readRuntimeSeen,
20
21
  writeRuntimeSeen,
21
22
  } from "./capability/capability.store.ts";
@@ -133,7 +134,7 @@ import {
133
134
  import { operatorBootstrapLines } from "./policy/policy.operator.ts";
134
135
  import { isOperatorMode, OPERATOR_MODES } from "./policy/policy.posture.ts";
135
136
  import { activeRails } from "./policy/policy.rails.ts";
136
- import { pruneShadowed, shadowedKeys } from "./policy/policy.shadow.ts";
137
+ import { pruneShadowed, shadowedKeys, typeMismatches, unknownKeys } from "./policy/policy.shadow.ts";
137
138
  import { forProvider } from "./policy/policy.types.ts";
138
139
  import { checkCollision, heartbeat, register, release, sweepStale } from "./presence/presence.service.ts";
139
140
  import { freshness, freshnessMessage, mayReplace, shouldRefetch } from "./pricing/pricing.freshness.ts";
@@ -264,6 +265,7 @@ export const coreFacade = {
264
265
  ENABLE_HINT,
265
266
  loadCatalog,
266
267
  readProjectPolicyRaw,
268
+ readProjectPolicyStatus,
267
269
  readRuntimeSeen,
268
270
  writeRuntimeSeen,
269
271
  isAvailableNotEnabled,
@@ -416,6 +418,8 @@ export const coreFacade = {
416
418
  resolvedWithoutProjectTier,
417
419
  shadowedKeys,
418
420
  pruneShadowed,
421
+ unknownKeys,
422
+ typeMismatches,
419
423
  resolveProjectPosture,
420
424
  resolveProjectSyncMode,
421
425
  OPERATOR_MODES,
@@ -11,7 +11,11 @@ function readJsonFile<T>(path: string): T | null {
11
11
  return null;
12
12
  }
13
13
  try {
14
- return JSON.parse(readFileSync(path, "utf8")) as T;
14
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
15
+ // why: a JSON Schema meta-key, not a Policy field — deepMerge's `...patch` spread would otherwise
16
+ // carry it straight into the merged runtime Policy object.
17
+ delete parsed.$schema;
18
+ return parsed as T;
15
19
  } catch {
16
20
  return null;
17
21
  }