fv-skills-baif 2.0.1 → 2.0.2

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,11 @@ All notable changes to FVS (Formal Verification Skills) will be documented in th
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## [2.0.2] - 2026-07-02
8
+
9
+ ### Fixed
10
+ - Codex installs now enable hooks with `[features].hooks = true` instead of an invalid root-level `hooks = true`, which current Codex parses as the hooks config table and rejects at startup.
11
+
7
12
  ## [2.0.1] - 2026-07-01
8
13
 
9
14
  Documentation polish. No code or behavior changes.
package/bin/install.js CHANGED
@@ -16,13 +16,20 @@ const reset = '\x1b[0m';
16
16
  // Codex config.toml constants
17
17
  const FVS_CODEX_MARKER = '# FVS Agent Configuration \u2014 managed by fv-skills-baif installer';
18
18
 
19
- // Codex hooks feature flag. Current Codex CLI reads a boolean `hooks` flag in
20
- // config.toml to enable its hook dispatcher; an older naming used `codex_hooks`.
21
- // Emit the canonical key and treat the legacy alias as equivalent so a reinstall
22
- // over an older config migrates the flag forward instead of leaving a duplicate.
19
+ // Codex hooks feature flag. Current Codex CLI reads feature flags under the
20
+ // `[features]` table; a root-level `hooks = true` is parsed as the hooks config
21
+ // object itself and is rejected. Emit `[features].hooks`, and treat the legacy
22
+ // alias as equivalent so a reinstall over an older config migrates forward
23
+ // instead of leaving a duplicate.
23
24
  const CODEX_HOOKS_FEATURE_KEY = 'hooks';
24
25
  const CODEX_HOOKS_FEATURE_LEGACY_KEYS = ['codex_hooks'];
25
26
  const CODEX_HOOKS_FEATURE_ALL_KEYS = [CODEX_HOOKS_FEATURE_KEY, ...CODEX_HOOKS_FEATURE_LEGACY_KEYS];
27
+ const CODEX_FEATURES_TABLE = 'features';
28
+ const FVS_CODEX_HOOKS_FEATURE_COMMENT = '# FVS-owned Codex hooks feature flag';
29
+
30
+ function isCodexHooksFeatureKey(key) {
31
+ return CODEX_HOOKS_FEATURE_ALL_KEYS.includes(key);
32
+ }
26
33
 
27
34
  // The only FVS hook script with a Codex event target is the update checker,
28
35
  // wired as a SessionStart hook. The statusline has no Codex renderer surface and
@@ -1083,6 +1090,123 @@ function removeContentRanges(content, ranges) {
1083
1090
  return cleaned;
1084
1091
  }
1085
1092
 
1093
+ function joinTomlLines(lines) {
1094
+ return lines.map((line) => line.text + line.eol).join('');
1095
+ }
1096
+
1097
+ function parseCodexHooksFeatureAssignment(lineText) {
1098
+ const m = lineText.match(/^\s*(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_]+))\s*=\s*(true|false)(\s*(?:#.*)?)?$/);
1099
+ if (!m) return null;
1100
+ const key = m[1] || m[2] || m[3];
1101
+ if (!isCodexHooksFeatureKey(key)) return null;
1102
+ return { key, value: m[4], suffix: m[5] || '' };
1103
+ }
1104
+
1105
+ function parseRootDottedCodexHooksFeatureAssignment(lineText) {
1106
+ const m = lineText.match(/^\s*features\.(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_]+))\s*=\s*(true|false)(\s*(?:#.*)?)?$/);
1107
+ if (!m) return null;
1108
+ const key = m[1] || m[2] || m[3];
1109
+ if (!isCodexHooksFeatureKey(key)) return null;
1110
+ return { key, value: m[4], suffix: m[5] || '' };
1111
+ }
1112
+
1113
+ function renderCodexHooksFeatureAssignment(lineText) {
1114
+ const indent = (lineText.match(/^\s*/) || [''])[0];
1115
+ const parsed = parseCodexHooksFeatureAssignment(lineText);
1116
+ return `${indent}${CODEX_HOOKS_FEATURE_KEY} = true${parsed ? parsed.suffix : ''}`;
1117
+ }
1118
+
1119
+ function renderRootDottedCodexHooksFeatureAssignment(lineText) {
1120
+ const indent = (lineText.match(/^\s*/) || [''])[0];
1121
+ const parsed = parseRootDottedCodexHooksFeatureAssignment(lineText);
1122
+ return `${indent}features.${CODEX_HOOKS_FEATURE_KEY} = true${parsed ? parsed.suffix : ''}`;
1123
+ }
1124
+
1125
+ function removeRootCodexHooksFeatureFlags(content) {
1126
+ const lines = splitTomlLines(content);
1127
+ const firstHeaderIdx = lines.findIndex((line) => parseTomlTableHeader(line.text));
1128
+ let changed = false;
1129
+ const kept = lines.filter((line, index) => {
1130
+ if (firstHeaderIdx !== -1 && index >= firstHeaderIdx) return true;
1131
+ if (!parseCodexHooksFeatureAssignment(line.text)) return true;
1132
+ changed = true;
1133
+ return false;
1134
+ });
1135
+ return { content: changed ? joinTomlLines(kept) : content, changed };
1136
+ }
1137
+
1138
+ function getCodexFeaturesSection(content) {
1139
+ return getTomlTableSections(content)
1140
+ .find((section) => !section.array && section.path === CODEX_FEATURES_TABLE);
1141
+ }
1142
+
1143
+ function rewriteRootDottedCodexHooksFeature(content) {
1144
+ const lines = splitTomlLines(content);
1145
+ const firstHeaderIdx = lines.findIndex((line) => parseTomlTableHeader(line.text));
1146
+ let found = false;
1147
+ let changed = false;
1148
+ const kept = [];
1149
+
1150
+ for (let i = 0; i < lines.length; i += 1) {
1151
+ const line = lines[i];
1152
+ if (firstHeaderIdx !== -1 && i >= firstHeaderIdx) {
1153
+ kept.push(line);
1154
+ continue;
1155
+ }
1156
+
1157
+ if (!parseRootDottedCodexHooksFeatureAssignment(line.text)) {
1158
+ kept.push(line);
1159
+ continue;
1160
+ }
1161
+
1162
+ if (!found) {
1163
+ found = true;
1164
+ const rendered = renderRootDottedCodexHooksFeatureAssignment(line.text);
1165
+ if (rendered !== line.text) changed = true;
1166
+ kept.push({ ...line, text: rendered });
1167
+ } else {
1168
+ changed = true;
1169
+ }
1170
+ }
1171
+
1172
+ return { content: changed ? joinTomlLines(kept) : content, found, changed };
1173
+ }
1174
+
1175
+ function stripFvsManagedCodexHooksFeature(content) {
1176
+ // Self-heal v2.0.1 installs, which wrote the now-invalid root `hooks = true`.
1177
+ let cleaned = removeRootCodexHooksFeatureFlags(content).content;
1178
+ const featuresSection = getCodexFeaturesSection(cleaned);
1179
+ if (!featuresSection) return cleaned;
1180
+
1181
+ const body = cleaned.slice(featuresSection.headerEnd, featuresSection.end);
1182
+ const lines = splitTomlLines(body);
1183
+ const kept = [];
1184
+ let removed = false;
1185
+
1186
+ for (let i = 0; i < lines.length; i += 1) {
1187
+ const line = lines[i];
1188
+ if (line.text.trim() === FVS_CODEX_HOOKS_FEATURE_COMMENT) {
1189
+ removed = true;
1190
+ const next = lines[i + 1];
1191
+ if (next && parseCodexHooksFeatureAssignment(next.text)) {
1192
+ i += 1;
1193
+ }
1194
+ continue;
1195
+ }
1196
+ kept.push(line);
1197
+ }
1198
+
1199
+ if (!removed) return cleaned;
1200
+
1201
+ const nextBody = joinTomlLines(kept);
1202
+ const hasRemainingContent = nextBody.split(/\r?\n/).some((line) => line.trim() !== '');
1203
+ if (!hasRemainingContent) {
1204
+ return cleaned.slice(0, featuresSection.start) + cleaned.slice(featuresSection.end);
1205
+ }
1206
+
1207
+ return cleaned.slice(0, featuresSection.headerEnd) + nextBody + cleaned.slice(featuresSection.end);
1208
+ }
1209
+
1086
1210
  /**
1087
1211
  * Strip FVS sections from Codex config.toml content.
1088
1212
  *
@@ -1129,21 +1253,11 @@ function stripFvsFromCodexConfig(content) {
1129
1253
  cleaned = before + after;
1130
1254
  }
1131
1255
 
1132
- // Remove the FVS-owned root-level hooks feature flag (canonical + legacy).
1133
- // The flag is inserted by ensureCodexHooksFeature between the FVS marker and
1134
- // the first FVS table, so once the marker and tables are gone it is left
1135
- // orphaned. Only strip it when the FVS marker was present — a flag in a
1136
- // config that FVS never marked is the user's and must be preserved.
1256
+ // Remove the FVS-owned hooks feature gate. v2.0.1 emitted an invalid root
1257
+ // `hooks = true`; current installs mark their `[features].hooks` insertion so
1258
+ // uninstall can remove only the FVS-owned line and preserve user/GSD flags.
1137
1259
  if (hadFvsMarker) {
1138
- const flagLines = cleaned.split(/\r?\n/);
1139
- const firstHeaderIdx = flagLines.findIndex((l) => /^\s*\[/.test(l));
1140
- cleaned = flagLines
1141
- .filter((line, i) => {
1142
- if (firstHeaderIdx !== -1 && i >= firstHeaderIdx) return true;
1143
- const m = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*true\s*$/);
1144
- return !(m && CODEX_HOOKS_FEATURE_ALL_KEYS.includes(m[1]));
1145
- })
1146
- .join('\n');
1260
+ cleaned = stripFvsManagedCodexHooksFeature(cleaned);
1147
1261
  }
1148
1262
 
1149
1263
  // Collapse runs of blank lines the removals may have left behind, then
@@ -1165,9 +1279,9 @@ function stripFvsFromCodexConfig(content) {
1165
1279
  *
1166
1280
  * On reinstall this first strips any previously-emitted FVS tables via the
1167
1281
  * TOML-aware strip (so foreign user/GSD tables survive verbatim and stale FVS
1168
- * tables are removed), then appends the freshly-generated FVS block. The
1169
- * FVS-emitted block carries no `[features]`/`multi_agent` keys, so none are
1170
- * injected into the user's config.
1282
+ * tables are removed), then appends the freshly-generated FVS agent block. The
1283
+ * hook feature gate is handled separately by ensureCodexHooksFeature so it can
1284
+ * coexist with user/GSD `[features]` settings.
1171
1285
  */
1172
1286
  function mergeCodexConfig(configPath, fvsBlock) {
1173
1287
  // Case 1: No config.toml -- create fresh.
@@ -1539,62 +1653,103 @@ function removeCodexHooksJsonSessionStart(targetDir) {
1539
1653
  return reconcileCodexHooksJsonSessionStart(targetDir, { managedCommand: null });
1540
1654
  }
1541
1655
 
1542
- // Ensure the Codex hooks feature flag is present and set true under the
1543
- // canonical key. If the file already enables the flag under the legacy key,
1544
- // rewrite that line to the canonical key (migrate forward) rather than adding a
1545
- // duplicate. Returns { content, changed }.
1656
+ // Ensure the Codex hooks feature flag is present as `[features].hooks = true`.
1657
+ // Older FVS versions wrote root-level `hooks = true`, which current Codex parses
1658
+ // as the hooks config table and rejects; remove that stale shape while inserting
1659
+ // the valid feature flag.
1546
1660
  function ensureCodexHooksFeature(configContent) {
1547
- const content = typeof configContent === 'string' ? configContent : '';
1661
+ const original = typeof configContent === 'string' ? configContent : '';
1662
+ const rootStripped = removeRootCodexHooksFeatureFlags(original);
1663
+ let content = rootStripped.content;
1548
1664
  const eol = content ? detectLineEnding(content) : '\n';
1549
- const lines = content.length ? content.split(/\r?\n/) : [];
1550
1665
 
1551
- let canonicalIdx = -1;
1552
- let legacyIdx = -1;
1553
- for (let i = 0; i < lines.length; i += 1) {
1554
- const m = lines[i].match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.+?)\s*$/);
1555
- if (!m) continue;
1556
- const key = m[1];
1557
- if (key === CODEX_HOOKS_FEATURE_KEY && canonicalIdx === -1) canonicalIdx = i;
1558
- else if (CODEX_HOOKS_FEATURE_LEGACY_KEYS.includes(key) && legacyIdx === -1) legacyIdx = i;
1559
- }
1560
-
1561
- // Only operate on root-level flag lines (not inside a [table]); a flag line
1562
- // appearing after the first [section] header is treated as section-scoped and
1563
- // left alone — FVS emits the flag at the document root before any table.
1564
- const firstHeaderIdx = lines.findIndex((l) => /^\s*\[/.test(l));
1565
- const isRootLevel = (idx) => idx !== -1 && (firstHeaderIdx === -1 || idx < firstHeaderIdx);
1566
-
1567
- if (isRootLevel(canonicalIdx)) {
1568
- if (/=\s*true\s*$/.test(lines[canonicalIdx])) return { content, changed: false };
1569
- lines[canonicalIdx] = `${CODEX_HOOKS_FEATURE_KEY} = true`;
1570
- return { content: lines.join(eol), changed: true };
1571
- }
1572
- if (isRootLevel(legacyIdx)) {
1573
- lines[legacyIdx] = `${CODEX_HOOKS_FEATURE_KEY} = true`;
1574
- return { content: lines.join(eol), changed: true };
1575
- }
1576
-
1577
- // Insert a fresh canonical flag line at the document root, above any table.
1578
- const insertLine = `${CODEX_HOOKS_FEATURE_KEY} = true`;
1579
- if (firstHeaderIdx === -1) {
1580
- const base = content.trimEnd();
1581
- const next = base ? `${insertLine}${eol}${base}${eol}` : `${insertLine}${eol}`;
1666
+ const featuresSection = getCodexFeaturesSection(content);
1667
+ if (featuresSection) {
1668
+ const body = content.slice(featuresSection.headerEnd, featuresSection.end);
1669
+ const lines = splitTomlLines(body);
1670
+ let found = false;
1671
+ let changed = rootStripped.changed;
1672
+ const kept = [];
1673
+
1674
+ for (const line of lines) {
1675
+ const parsed = parseCodexHooksFeatureAssignment(line.text);
1676
+ if (!parsed) {
1677
+ kept.push(line);
1678
+ continue;
1679
+ }
1680
+
1681
+ if (!found) {
1682
+ found = true;
1683
+ const rendered = renderCodexHooksFeatureAssignment(line.text);
1684
+ if (rendered !== line.text) changed = true;
1685
+ kept.push({ ...line, text: rendered });
1686
+ } else {
1687
+ changed = true;
1688
+ }
1689
+ }
1690
+
1691
+ if (found) {
1692
+ const next = content.slice(0, featuresSection.headerEnd) +
1693
+ joinTomlLines(kept) +
1694
+ content.slice(featuresSection.end);
1695
+ return { content: next, changed: changed || next !== original };
1696
+ }
1697
+
1698
+ const needsLineBreak = featuresSection.end > 0 &&
1699
+ !content.slice(0, featuresSection.end).endsWith('\n') &&
1700
+ !content.slice(0, featuresSection.end).endsWith('\r\n');
1701
+ const insertText = `${needsLineBreak ? eol : ''}${FVS_CODEX_HOOKS_FEATURE_COMMENT}${eol}${CODEX_HOOKS_FEATURE_KEY} = true${eol}`;
1702
+ const next = content.slice(0, featuresSection.end) + insertText + content.slice(featuresSection.end);
1703
+ return { content: next, changed: true };
1704
+ }
1705
+
1706
+ const dotted = rewriteRootDottedCodexHooksFeature(content);
1707
+ if (dotted.found) {
1708
+ return { content: dotted.content, changed: rootStripped.changed || dotted.changed || dotted.content !== original };
1709
+ }
1710
+
1711
+ const featuresBlock = [
1712
+ `[${CODEX_FEATURES_TABLE}]`,
1713
+ FVS_CODEX_HOOKS_FEATURE_COMMENT,
1714
+ `${CODEX_HOOKS_FEATURE_KEY} = true`,
1715
+ '',
1716
+ ].join(eol);
1717
+
1718
+ if (!content) return { content: featuresBlock, changed: true };
1719
+
1720
+ const firstTable = getTomlTableSections(content)[0];
1721
+ if (firstTable) {
1722
+ const before = content.slice(0, firstTable.start);
1723
+ const after = content.slice(firstTable.start);
1724
+ const needsGap = before.length > 0 && !before.endsWith(eol + eol);
1725
+ const next = before + (needsGap ? eol : '') + featuresBlock + eol + after;
1582
1726
  return { content: next, changed: true };
1583
1727
  }
1584
- lines.splice(firstHeaderIdx, 0, insertLine, '');
1585
- return { content: lines.join(eol), changed: true };
1728
+
1729
+ const base = content.trimEnd();
1730
+ const needsGap = base.length > 0;
1731
+ const next = `${base}${needsGap ? eol + eol : ''}${featuresBlock}`;
1732
+ return { content: next, changed: true };
1586
1733
  }
1587
1734
 
1588
1735
  function hasEnabledCodexHooksFeature(configContent) {
1589
1736
  if (typeof configContent !== 'string') return false;
1590
- const firstHeaderIdx = configContent.split(/\r?\n/).findIndex((l) => /^\s*\[/.test(l));
1591
- const lines = configContent.split(/\r?\n/);
1592
- for (let i = 0; i < lines.length; i += 1) {
1593
- if (firstHeaderIdx !== -1 && i >= firstHeaderIdx) break;
1594
- const m = lines[i].match(/^\s*([A-Za-z0-9_]+)\s*=\s*true\s*$/);
1595
- if (m && CODEX_HOOKS_FEATURE_ALL_KEYS.includes(m[1])) return true;
1737
+ const featuresSection = getCodexFeaturesSection(configContent);
1738
+ if (featuresSection) {
1739
+ const body = configContent.slice(featuresSection.headerEnd, featuresSection.end);
1740
+ for (const line of splitTomlLines(body)) {
1741
+ const parsed = parseCodexHooksFeatureAssignment(line.text);
1742
+ if (parsed && parsed.value === 'true') return true;
1743
+ }
1596
1744
  }
1597
- return false;
1745
+
1746
+ const lines = splitTomlLines(configContent);
1747
+ const firstHeaderIdx = lines.findIndex((line) => parseTomlTableHeader(line.text));
1748
+ return lines.some((line, index) => {
1749
+ if (firstHeaderIdx !== -1 && index >= firstHeaderIdx) return false;
1750
+ const parsed = parseRootDottedCodexHooksFeatureAssignment(line.text);
1751
+ return Boolean(parsed && parsed.value === 'true');
1752
+ });
1598
1753
  }
1599
1754
 
1600
1755
  /**
package/fv-skills/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.1
1
+ 2.0.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fv-skills-baif",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "Formal verification skills for Claude Code, Codex, OpenCode, and Gemini. Rust -> Lean 4 via Aeneas.",
5
5
  "bin": {
6
6
  "fv-skills-baif": "bin/install.js"