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 +5 -0
- package/bin/install.js +222 -67
- package/fv-skills/VERSION +1 -1
- package/package.json +1 -1
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
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// over an older config migrates
|
|
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
|
|
1133
|
-
//
|
|
1134
|
-
//
|
|
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
|
-
|
|
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
|
-
*
|
|
1170
|
-
*
|
|
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
|
|
1543
|
-
//
|
|
1544
|
-
//
|
|
1545
|
-
//
|
|
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
|
|
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
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
const
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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
|
-
|
|
1585
|
-
|
|
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
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
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
|
-
|
|
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
|
+
2.0.2
|
package/package.json
CHANGED