arkgate 2.8.2 → 2.8.3

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/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@ All notable changes to ArkGate (`arkgate`; formerly `ark-runtime-kernel`) are do
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 2.8.3 — 2026-07-09
8
+
9
+ Field residuals + official site: core ratchet to honest ENFORCE, typecheck bootstrap,
10
+ host-token scrub, arkgate.online homepage. **No intentional CLI flag or JSON shape breaks.**
11
+
12
+ ### Added
13
+
14
+ - **`--ratchet-cores`:** when architecture is green (0 active violations, governed ≥ 50%),
15
+ set `optional: false` on **populated** core layers only so doctor can report **ENFORCE**
16
+ honestly. Empty cores stay optional (no false-ENFORCE theatre). Doctor core-optional gaps
17
+ point at this command; `/ark-autopilot` documents the step after goal.met.
18
+ - **Typecheck bootstrap:** `ark start` / `--install-agent-gates` add `"typecheck": "tsc --noEmit"`
19
+ when `tsconfig.json`/`jsconfig.json` exists and no typecheck-like script is present; generated
20
+ CI includes the typecheck step. Existing scripts are never overwritten.
21
+
22
+ ### Changed
23
+
24
+ - **Official website:** product homepage is [arkgate.online](https://www.arkgate.online/)
25
+ (`package.json` `homepage`, README badges/footer, ROADMAP identity). npm package page
26
+ and GitHub remain source + distribution links.
27
+ - **Repo hygiene:** scrub named field-probe host identities from docs, comments, tests, and
28
+ historical changelog wording; fixtures stay framework-generic (Nest/Next only).
29
+ - **Maintainability:** `--ratchet-cores` lives in `bin/lib/core-ratchet.mjs` (not the ark-check
30
+ entry); typecheck detection uses shared `packageScriptsHaveTypecheck` (not full deploy-path
31
+ scan); typecheck bootstrap is skipped under `--skills-only`.
32
+
33
+
7
34
  ## 2.8.2 — 2026-07-09
8
35
 
9
36
  Field-honesty patch (Next/UI host probe): no Nest false positives, no false ENFORCE on
@@ -112,7 +139,7 @@ shape breaks** for the gate/co-pilot path.
112
139
 
113
140
  ## 2.6.1 — 2026-07-09
114
141
 
115
- Field-test release: Next/monorepo honesty (deer-flow-style hosts), simplified **one-flow** UX for
142
+ Field-test release: Next/monorepo honesty (frontend monorepo hosts), simplified **one-flow** UX for
116
143
  humans and autonomous agents, and skills that require real source remediation—not CLI paraphrase.
117
144
 
118
145
  ### Fixed — false greens & strict CI noise (Next / monorepo)
@@ -160,7 +187,7 @@ Templates under `templates/skills/` (and project `.grok/skills` copies) for at l
160
187
 
161
188
  ### Tests
162
189
 
163
- - Fixture-style unit tests drive real `bin/ark-check.mjs`: Next core governance; deer-flow-like
190
+ - Fixture-style unit tests drive real `bin/ark-check.mjs`: Next core governance; frontend monorepo-like
164
191
  monorepo (`frontend` + `core/**/types.ts` not Domain); strict-config with dead globs;
165
192
  empty baseline removal.
166
193
 
package/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  Your AI writes most of the code. ArkGate keeps that code inside an architecture you can trust —
8
8
  and makes sure a “green” check means something real.
9
9
 
10
+ [![Website](https://img.shields.io/badge/website-arkgate.online-0a0a0a)](https://www.arkgate.online/)
10
11
  [![CI](https://github.com/pedroknigge/arkgate/actions/workflows/ci.yml/badge.svg)](https://github.com/pedroknigge/arkgate/actions/workflows/ci.yml)
11
12
  [![npm](https://img.shields.io/npm/v/arkgate?color=cb3837&label=npm)](https://www.npmjs.com/package/arkgate)
12
13
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
@@ -267,10 +268,12 @@ npm run typecheck
267
268
  npm run check:architecture # Ark gates itself
268
269
  ```
269
270
 
271
+ **Website:** [arkgate.online](https://www.arkgate.online/)
270
272
  **npm:** [`arkgate`](https://www.npmjs.com/package/arkgate) · formerly `ark-runtime-kernel`
271
273
  **Product:** **ArkGate** — architecture co-pilot / gate for AI TypeScript (not a runtime kernel).
272
274
  CLI: `arkgate` · `arkgate-check` · `arkgate-mcp` (aliases `ark` / `ark-check` / `ark-mcp` still work for one major).
273
- MCP registry: `io.github.pedroknigge/arkgate`.
275
+ MCP registry: `io.github.pedroknigge/arkgate`.
276
+ **Source:** [github.com/pedroknigge/arkgate](https://github.com/pedroknigge/arkgate)
274
277
 
275
278
  Node ≥ 18 · **MIT**.
276
279
 
package/bin/ark-check.mjs CHANGED
@@ -60,6 +60,7 @@ import {
60
60
  runPlan,
61
61
  runDoctor,
62
62
  } from './lib/doctor-plan.mjs';
63
+ import { runRatchetCores } from './lib/core-ratchet.mjs';
63
64
  import {
64
65
  baselineKey,
65
66
  readBaseline,
@@ -153,6 +154,7 @@ function parseArgs(argv) {
153
154
  else if (arg === '--apply-policy-pack') args.applyPolicyPack = argv[++i];
154
155
  else if (arg === '--suggest-include') args.suggestInclude = true;
155
156
  else if (arg === '--adopt-contract') args.adoptContract = true;
157
+ else if (arg === '--ratchet-cores') args.ratchetCores = true;
156
158
  else if (arg === '--write') args.write = true;
157
159
  else if (arg === '--watch') args.watch = true;
158
160
  else if (arg === '--beginner') args.beginner = true;
@@ -203,6 +205,7 @@ function usage() {
203
205
  ' ark-check --apply-policy-pack <id> [--force] write ark.config.json from templates/policy-packs/ (uses preset factory)',
204
206
  ' ark-check --suggest-include [--json] propose include roots (TS packages / workspaces)',
205
207
  ' ark-check --adopt-contract [--write] expand include + UI patterns from ungoverned dirs (contract adopt)',
208
+ ' ark-check --ratchet-cores when raw graph is green (0 violations; baseline ignored), set optional:false on populated cores only (writes ark.config.json)',
206
209
  ' ark-check --watch re-run the check when governed files change (debounced)',
207
210
  ' ark-check --report [file.html] [--beginner] [--reset-origin] [--no-archive]',
208
211
  ' HTML report + snapshots under .ark/reports/ (origin once, latest each run, history JSON)',
@@ -1046,6 +1049,11 @@ async function main() {
1046
1049
  return;
1047
1050
  }
1048
1051
 
1052
+ if (args.ratchetCores) {
1053
+ runRatchetCores(root, config, files, rules, violations, args, { displayPathFromRoot });
1054
+ return;
1055
+ }
1056
+
1049
1057
  if (args.updateBaseline) {
1050
1058
  const summary = summarizeViolations(violations);
1051
1059
  // Bloquear y avisar: a lopsided freeze buries a likely contract bug as "debt". Refuse it
@@ -1053,7 +1053,7 @@ export function collectRepoShapeSignals(root) {
1053
1053
  const pkg = readPackageJson(root);
1054
1054
  const workspaceDirs = detectWorkspaces(root);
1055
1055
  const workspaces = workspaceDirs.length > 0;
1056
- // Include frontend/web/client — common Next monorepo app folders (deer-flow-style).
1056
+ // Include frontend/web/client — common Next monorepo app folders.
1057
1057
  const candidateScanDirs = [
1058
1058
  'src',
1059
1059
  'lib',
package/bin/ark.mjs CHANGED
@@ -498,6 +498,11 @@ async function start(args) {
498
498
  console.log(' → origin report, adoption, plan, safe fixes, leave gates on.');
499
499
  console.log(` 2. Status anytime: ${arkCommand(root, 'ark-check', '--doctor')}`);
500
500
  console.log(` 3. After edits: ${arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config')}`);
501
+ if (mode === 'adapt' && planOk) {
502
+ console.log(
503
+ ` 4. When green but cores still optional: ${arkCommand(root, 'ark-check', '--ratchet-cores')} → honest ENFORCE`
504
+ );
505
+ }
501
506
  console.log('');
502
507
  console.log('Optional later: --plan · --coverage · /ark-fix · /ark-place · ark upgrade');
503
508
 
@@ -20,8 +20,9 @@ import {
20
20
  DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
21
21
  DEFAULT_RULES,
22
22
  createElevenLayerConfig,
23
- applyFrameworkLayoutOverlays
23
+ applyFrameworkLayoutOverlays,
24
24
  } from '../ark-shared.mjs';
25
+ import { CORE_LAYER_NAMES } from './core-ratchet.mjs';
25
26
  import {
26
27
  assessCodexHomeMcp,
27
28
  codexArkBlockHasPreferredBin,
@@ -76,6 +77,97 @@ export function hasCheckArchitectureScript(root) {
76
77
  return Boolean(pkg?.scripts?.['check:architecture']);
77
78
  }
78
79
 
80
+ /**
81
+ * Whether package.json scripts already expose a typecheck-like command.
82
+ * Shared by deploy-path quality + typecheck bootstrap (single definition).
83
+ * @param {Record<string, unknown>|null|undefined} scripts
84
+ */
85
+ export function packageScriptsHaveTypecheck(scripts) {
86
+ if (!scripts || typeof scripts !== 'object') return false;
87
+ return Boolean(
88
+ (typeof scripts.typecheck === 'string' && scripts.typecheck.trim()) ||
89
+ (typeof scripts['type-check'] === 'string' && scripts['type-check'].trim()) ||
90
+ (typeof scripts['check:types'] === 'string' && scripts['check:types'].trim()) ||
91
+ (typeof scripts.tsc === 'string' && /\btsc\b/.test(scripts.tsc))
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Root package (and shallow nested packages) already have a typecheck script.
97
+ * Does not scan CI or framework configs — only package.json scripts.
98
+ * @param {string} root
99
+ */
100
+ export function treeHasTypecheckScript(root) {
101
+ const pkg = readPackageJson(root);
102
+ if (packageScriptsHaveTypecheck(pkg?.scripts)) return true;
103
+ try {
104
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
105
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
106
+ const candidates = [path.join(root, entry.name)];
107
+ try {
108
+ for (const child of fs.readdirSync(path.join(root, entry.name), { withFileTypes: true })) {
109
+ if (child.isDirectory() && !child.name.startsWith('.')) {
110
+ candidates.push(path.join(root, entry.name, child.name));
111
+ }
112
+ }
113
+ } catch {
114
+ /* ignore */
115
+ }
116
+ for (const dir of candidates) {
117
+ const pj = path.join(dir, 'package.json');
118
+ if (!fs.existsSync(pj)) continue;
119
+ try {
120
+ const nested = JSON.parse(fs.readFileSync(pj, 'utf8'));
121
+ if (packageScriptsHaveTypecheck(nested.scripts)) return true;
122
+ } catch {
123
+ /* ignore */
124
+ }
125
+ }
126
+ }
127
+ } catch {
128
+ /* ignore */
129
+ }
130
+ return false;
131
+ }
132
+
133
+ /**
134
+ * Add a conservative `typecheck` script when the host has a TS/JS project config
135
+ * but no typecheck-like script yet. Never overwrites an existing script.
136
+ *
137
+ * @param {string} root
138
+ * @param {{ write?: boolean }} [opts]
139
+ * @returns {{
140
+ * changed: boolean,
141
+ * reason: 'added' | 'already' | 'no-tsconfig' | 'no-package-json',
142
+ * script?: string,
143
+ * }}
144
+ */
145
+ export function ensureTypecheckScript(root, opts = {}) {
146
+ const write = opts.write !== false;
147
+ const hasTsconfig =
148
+ fs.existsSync(path.join(root, 'tsconfig.json')) ||
149
+ fs.existsSync(path.join(root, 'jsconfig.json'));
150
+ if (!hasTsconfig) return { changed: false, reason: 'no-tsconfig' };
151
+
152
+ const pkgPath = path.join(root, 'package.json');
153
+ if (!fs.existsSync(pkgPath)) return { changed: false, reason: 'no-package-json' };
154
+
155
+ if (treeHasTypecheckScript(root)) {
156
+ return { changed: false, reason: 'already' };
157
+ }
158
+
159
+ const pkg = readPackageJson(root) || {};
160
+ const scripts =
161
+ pkg.scripts && typeof pkg.scripts === 'object' ? { ...pkg.scripts } : {};
162
+ const script = 'tsc --noEmit';
163
+ scripts.typecheck = script;
164
+ if (write) {
165
+ const next = { ...pkg, scripts };
166
+ fs.writeFileSync(pkgPath, `${JSON.stringify(next, null, 2)}\n`);
167
+ }
168
+ return { changed: true, reason: 'added', script };
169
+ }
170
+
79
171
  export const REQUIRED_GATE_FILES = [
80
172
  'AGENTS.md',
81
173
  '.mcp.json',
@@ -945,14 +1037,6 @@ export function brokenMcpGateFiles(root) {
945
1037
  return bad;
946
1038
  }
947
1039
 
948
- /** Core layers whose optionality matters once they match files (presets share these names). */
949
- const CORE_LAYER_NAMES = new Set([
950
- 'DomainModel',
951
- 'ApplicationOrchestration',
952
- 'PresentationAdapters',
953
- 'PersistenceAdapters',
954
- ]);
955
-
956
1040
  /**
957
1041
  * Production deploy path quality (universal — any consumer repo).
958
1042
  * Detects when the production build host runs ESLint / typecheck as part of
@@ -1005,17 +1089,9 @@ export function detectDeployPathQuality(root) {
1005
1089
  (typeof s['lint:ci'] === 'string' && s['lint:ci'].trim()) ||
1006
1090
  (typeof s['check:lint'] === 'string' && s['check:lint'].trim()))
1007
1091
  );
1008
- const scriptHasTypecheck = (s) =>
1009
- Boolean(
1010
- s &&
1011
- ((typeof s.typecheck === 'string' && s.typecheck.trim()) ||
1012
- (typeof s['type-check'] === 'string' && s['type-check'].trim()) ||
1013
- (typeof s['check:types'] === 'string' && s['check:types'].trim()) ||
1014
- (typeof s.tsc === 'string' && /\btsc\b/.test(s.tsc)))
1015
- );
1016
1092
 
1017
1093
  let hasLintScript = scriptHasLint(scripts);
1018
- let hasTypecheckScript = scriptHasTypecheck(scripts);
1094
+ let hasTypecheckScript = packageScriptsHaveTypecheck(scripts);
1019
1095
  const packageLintScripts = [];
1020
1096
  // Monorepo: package-level scripts count (apps/web, packages/ui, …).
1021
1097
  try {
@@ -1042,7 +1118,7 @@ export function detectDeployPathQuality(root) {
1042
1118
  hasLintScript = true;
1043
1119
  packageLintScripts.push(path.relative(root, dir).split(path.sep).join('/'));
1044
1120
  }
1045
- if (scriptHasTypecheck(ns)) hasTypecheckScript = true;
1121
+ if (packageScriptsHaveTypecheck(ns)) hasTypecheckScript = true;
1046
1122
  const nd = {
1047
1123
  ...(nested.dependencies || {}),
1048
1124
  ...(nested.devDependencies || {}),
@@ -1289,7 +1365,7 @@ export function collectAdoptionGaps(root, config, coverage) {
1289
1365
  id: `core-optional-${layer.name}`,
1290
1366
  severity: 'info',
1291
1367
  message: `Core layer ${layer.name} has ${files} file(s) but is still optional: true — contract is weaker than the tree`,
1292
- fix: `Edit ark.config.json: remove optional on ${layer.name} (or set false), then ${arkCommand(root, 'ark-check', '--strict-config')}`,
1368
+ fix: `${arkCommand(root, 'ark-check', '--ratchet-cores')} (when architecture is green: 0 active violations)`,
1293
1369
  });
1294
1370
  }
1295
1371
  }
@@ -1625,7 +1701,15 @@ export function runInstallAgentGates(args) {
1625
1701
  // overwrite (they track the package). The gate/instruction files (AGENTS.md,
1626
1702
  // settings.json, CI workflow, rules) are the ones users customize, so a plain
1627
1703
  // `--force` clobbers them — this is the safe way to pick up new skill versions.
1704
+ // Do not mutate package.json under --skills-only (typecheck bootstrap is gates/CI).
1628
1705
  if (!args.skillsOnly) {
1706
+ // Bootstrap typecheck before CI template so generated workflow includes the step.
1707
+ const typecheckBootstrap = ensureTypecheckScript(root, { write: true });
1708
+ if (typecheckBootstrap.changed && !args.json) {
1709
+ console.log(
1710
+ `Added package.json script "typecheck": "${typecheckBootstrap.script}" (tsconfig present; local/CI parity).`
1711
+ );
1712
+ }
1629
1713
  // Base gates: tool-agnostic contract + CI backstop, always written.
1630
1714
  templates.push(['AGENTS.md', agentInstructions(root)]);
1631
1715
  templates.push(['.mcp.json', mcpJson(root)]);
@@ -127,7 +127,7 @@ export function collectConfigWarnings(root, config, files, rules, manifest) {
127
127
  // Advisory only under --strict-config: monorepo/Next presets ship many optional-looking
128
128
  // globs (e.g. src/layouts/**, app/**) that never match when include is ["frontend"].
129
129
  // Failing the release gate on dead preset globs caused false CI red while architecture
130
- // edges were clean (deer-flow host validation). Real safety is import violations +
130
+ // edges were clean on multi-package hosts. Real safety is import violations +
131
131
  // CONFIG_UNCLASSIFIED_FILES / invalid patterns.
132
132
  warnings.push(
133
133
  configWarning(
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Core-layer optionality ratchet — pure plan + CLI runner.
3
+ * Keeps ark-check.mjs orchestration-only (dispatch only).
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { arkCommand } from '../ark-shared.mjs';
8
+ import { computeCoverage } from './doctor-plan.mjs';
9
+
10
+ /**
11
+ * Core layers whose optionality matters once they match files (presets share these names).
12
+ * Used by doctor adoption gaps and `--ratchet-cores`.
13
+ */
14
+ export const CORE_LAYER_NAMES = new Set([
15
+ 'DomainModel',
16
+ 'ApplicationOrchestration',
17
+ 'PresentationAdapters',
18
+ 'PersistenceAdapters',
19
+ ]);
20
+
21
+ /**
22
+ * Plan a ratchet of optional→required for core layers that already match files.
23
+ * Empty cores stay optional (avoids false ENFORCE theatre). Pure — does not write disk.
24
+ *
25
+ * @param {object} config ark.config.json shape
26
+ * @param {{ name: string, files: number }[]} layerRows coverage layer rows
27
+ */
28
+ export function planPopulatedCoreRatchet(config, layerRows = []) {
29
+ const countByName = new Map(
30
+ (Array.isArray(layerRows) ? layerRows : []).map((row) => [row.name, Number(row.files) || 0])
31
+ );
32
+ const ratcheted = [];
33
+ const alreadyStrict = [];
34
+ const stillOptionalEmpty = [];
35
+ const nextLayers = (config?.layers ?? []).map((layer) => {
36
+ if (!CORE_LAYER_NAMES.has(layer.name)) return layer;
37
+ const files = countByName.get(layer.name) ?? 0;
38
+ if (layer.optional !== true) {
39
+ if (files > 0) alreadyStrict.push({ layer: layer.name, files });
40
+ return layer;
41
+ }
42
+ if (files <= 0) {
43
+ stillOptionalEmpty.push(layer.name);
44
+ return layer;
45
+ }
46
+ ratcheted.push({ layer: layer.name, files });
47
+ return { ...layer, optional: false };
48
+ });
49
+ return {
50
+ ratcheted,
51
+ alreadyStrict,
52
+ stillOptionalEmpty,
53
+ config: { ...(config ?? {}), layers: nextLayers },
54
+ changed: ratcheted.length > 0,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * When the architecture is green (raw violations = 0, not baselined), ratchet populated
60
+ * core layers from optional→required so doctor can honestly report ENFORCE.
61
+ * Empty cores stay optional. Always writes when changes apply (like --update-baseline).
62
+ *
63
+ * @param {string} root
64
+ * @param {object} config
65
+ * @param {string[]} files
66
+ * @param {object[]} rules
67
+ * @param {object[]} violations raw scan violations (baseline ignored — must be truly clean)
68
+ * @param {{ json?: boolean, config?: string }} args
69
+ * @param {{ displayPathFromRoot: (root: string, abs: string) => string }} deps
70
+ */
71
+ export function runRatchetCores(root, config, files, rules, violations, args, deps) {
72
+ const displayPathFromRoot = deps.displayPathFromRoot;
73
+ const cov = computeCoverage(root, config, files, rules);
74
+ const activeCount = Array.isArray(violations) ? violations.length : 0;
75
+ const configPath = path.isAbsolute(args.config)
76
+ ? args.config
77
+ : path.join(root, args.config || 'ark.config.json');
78
+
79
+ const refuse = (code, message, extra = {}) => {
80
+ if (args.json) {
81
+ console.log(JSON.stringify({ ok: false, error: message, ...extra }, null, 2));
82
+ } else {
83
+ console.error(message);
84
+ }
85
+ process.exitCode = code;
86
+ };
87
+
88
+ if (activeCount > 0) {
89
+ refuse(
90
+ 2,
91
+ `Refusing --ratchet-cores: ${activeCount} active architecture violation(s) (raw graph; baseline does not count). Resolve them first (ark-check --plan), then re-run.`,
92
+ { activeViolations: activeCount, governed: cov.governed }
93
+ );
94
+ return;
95
+ }
96
+ if (cov.totalFiles === 0 || (cov.governed?.percent ?? 0) < 50) {
97
+ refuse(
98
+ 2,
99
+ `Refusing --ratchet-cores: governed coverage is too low (${cov.governed?.percent ?? 0}% of ${cov.totalFiles} files). Classify ungoverned code first.`,
100
+ { governed: cov.governed }
101
+ );
102
+ return;
103
+ }
104
+
105
+ const plan = planPopulatedCoreRatchet(config, cov.layers);
106
+ if (!plan.changed) {
107
+ const payload = {
108
+ ok: true,
109
+ changed: false,
110
+ message: 'No optional core layers with files — nothing to ratchet.',
111
+ alreadyStrict: plan.alreadyStrict,
112
+ stillOptionalEmpty: plan.stillOptionalEmpty,
113
+ governed: cov.governed,
114
+ };
115
+ if (args.json) console.log(JSON.stringify(payload, null, 2));
116
+ else {
117
+ console.log(payload.message);
118
+ if (plan.stillOptionalEmpty.length > 0) {
119
+ console.log(`Still optional (empty patterns): ${plan.stillOptionalEmpty.join(', ')}`);
120
+ }
121
+ }
122
+ return;
123
+ }
124
+
125
+ fs.writeFileSync(configPath, `${JSON.stringify(plan.config, null, 2)}\n`);
126
+ const payload = {
127
+ ok: true,
128
+ changed: true,
129
+ configPath: displayPathFromRoot(root, configPath),
130
+ ratcheted: plan.ratcheted,
131
+ stillOptionalEmpty: plan.stillOptionalEmpty,
132
+ governed: cov.governed,
133
+ next: arkCommand(root, 'ark-check', '--doctor'),
134
+ };
135
+ if (args.json) {
136
+ console.log(JSON.stringify(payload, null, 2));
137
+ } else {
138
+ console.log(
139
+ `Ratcheted ${plan.ratcheted.length} core layer(s) to optional: false (populated only):`
140
+ );
141
+ for (const row of plan.ratcheted) {
142
+ console.log(` ${row.layer} (${row.files} file(s))`);
143
+ }
144
+ if (plan.stillOptionalEmpty.length > 0) {
145
+ console.log(
146
+ `Left optional (empty patterns — avoid false ENFORCE): ${plan.stillOptionalEmpty.join(', ')}`
147
+ );
148
+ }
149
+ console.log(`Wrote ${displayPathFromRoot(root, configPath)}`);
150
+ console.log(`Confirm: ${payload.next}`);
151
+ }
152
+ }
package/dist/index.cjs CHANGED
@@ -80,7 +80,7 @@ __export(index_exports, {
80
80
  module.exports = __toCommonJS(index_exports);
81
81
 
82
82
  // src/version.ts
83
- var version = "2.8.2";
83
+ var version = "2.8.3";
84
84
 
85
85
  // src/kernel/intent/IntentRegistry.ts
86
86
  var IntentRegistry = class {