fix(3566): emit canonical [features].hooks; recognize legacy codex_hooks as alias
Closes #3566 Codex itself marks codex_hooks as a legacy_key in codex-rs/features/src/legacy.rs. The canonical current Codex feature flag under [features] is hooks. The GSD installer was still writing codex_hooks on every fresh install / reinstall, leaving deprecated config behind on Codex CLI >= 0.130.0. Introduces a canonical/legacy split in bin/install.js: CODEX_HOOKS_FEATURE_KEY = 'hooks' CODEX_HOOKS_FEATURE_LEGACY_KEYS = ['codex_hooks'] isCodexHooksFeatureKey(key) // recognizes canonical OR any legacy alias Threaded through: - ensureCodexHooksFeature(): emits canonical hooks; recognizes legacy codex_hooks; migrates legacy -> canonical in section, root-dotted, and block-fallback insertion paths. - hasEnabledCodexHooksFeature(): accepts either canonical or legacy. - stripCodexHooksFeatureAssignments(): strips either canonical or legacy during uninstall when GSD owns the line. - rewriteTomlKeyLines(): now always uses the caller-supplied key instead of the parsed-record keyRaw. The old `match.keyRaw || key` fallback was the proximate reason the migration silently no-op'd — callers asking to rewrite a section line to `hooks` got back the legacy `codex_hooks` line because the parsed record carried keyRaw="codex_hooks". The GSD_CODEX_HOOKS_OWNERSHIP_PREFIX audit-marker string is intentionally unchanged so existing installs' ownership lines continue to round-trip. Tests: - New tests/bug-3566-codex-hooks-feature-canonical-key.test.cjs (6 cases): fresh install writes hooks; section-form legacy migrated; root-dotted legacy migrated; user-owned hooks preserved; uninstall removes GSD-owned canonical; uninstall preserves user-owned hooks. - Pre-existing legacy-pinning behaviour-change updates land in this PR via the rewriteTomlKeyLines + ensureCodexHooksFeature edits; the bug-2760-codex-install-defensive and bug-3427-3433 suites pass on the new shape without further test edits because they assert behaviour (not the literal key name). Docs: - docs/ARCHITECTURE.md row for Codex notes [features].hooks (canonical, legacy codex_hooks recognized and migrated forward). - docs/installer-migrations.md row updated to reflect canonical key and the new Codex 0.130.0 features.hooks compatibility sentinel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,19 @@ const reset = '\x1b[0m';
|
||||
// Codex config.toml constants
|
||||
const GSD_CODEX_MARKER = '# GSD Agent Configuration \u2014 managed by get-shit-done installer';
|
||||
const GSD_CODEX_HOOKS_OWNERSHIP_PREFIX = '# GSD codex_hooks ownership: ';
|
||||
// Codex's hook-enabling feature flag (issue #3566). Codex itself marks
|
||||
// `codex_hooks` as a `legacy_key` in codex-rs/features/src/legacy.rs; the
|
||||
// canonical current key under [features] is `hooks`. The installer always
|
||||
// emits the canonical key going forward, recognizes legacy aliases as
|
||||
// equivalent during reinstall, and migrates them forward on rewrite. The
|
||||
// audit-marker string above is intentionally unchanged so existing
|
||||
// installs' ownership lines continue to round-trip.
|
||||
const CODEX_HOOKS_FEATURE_KEY = 'hooks';
|
||||
const CODEX_HOOKS_FEATURE_LEGACY_KEYS = ['codex_hooks'];
|
||||
const CODEX_HOOKS_FEATURE_ALL_KEYS = [CODEX_HOOKS_FEATURE_KEY, ...CODEX_HOOKS_FEATURE_LEGACY_KEYS];
|
||||
function isCodexHooksFeatureKey(key) {
|
||||
return CODEX_HOOKS_FEATURE_ALL_KEYS.includes(key);
|
||||
}
|
||||
|
||||
// Copilot instructions marker constants
|
||||
const GSD_COPILOT_INSTRUCTIONS_MARKER = '<!-- GSD Configuration \u2014 managed by get-shit-done installer -->';
|
||||
@@ -3230,7 +3243,7 @@ function stripCodexHooksFeatureAssignments(content, ownership = null) {
|
||||
!record.startsInMultilineString &&
|
||||
record.keySegments &&
|
||||
record.keySegments.length === 1 &&
|
||||
record.keySegments[0] === 'codex_hooks'
|
||||
isCodexHooksFeatureKey(record.keySegments[0])
|
||||
);
|
||||
|
||||
for (const record of codexHookRecords) {
|
||||
@@ -3275,7 +3288,7 @@ function stripCodexHooksFeatureAssignments(content, ownership = null) {
|
||||
record.keySegments &&
|
||||
record.keySegments.length === 2 &&
|
||||
record.keySegments[0] === 'features' &&
|
||||
record.keySegments[1] === 'codex_hooks'
|
||||
isCodexHooksFeatureKey(record.keySegments[1])
|
||||
);
|
||||
|
||||
for (const record of rootCodexHookRecords) {
|
||||
@@ -4431,7 +4444,14 @@ function rewriteTomlKeyLines(content, matches, key) {
|
||||
const blockEol = blockEnd > 0 && content[blockEnd - 1] === '\n'
|
||||
? (blockEnd > 1 && content[blockEnd - 2] === '\r' ? '\r\n' : '\n')
|
||||
: '';
|
||||
rewritten += normalizeCodexHooksLine(match.text, match.keyRaw || key) + blockEol;
|
||||
// Always rewrite to the caller-supplied canonical `key`, ignoring
|
||||
// `match.keyRaw`. The previous `match.keyRaw || key` fallback
|
||||
// silently preserved legacy aliases (issue #3566): callers asking
|
||||
// to rewrite a section line to `hooks` would get back the original
|
||||
// `codex_hooks` line unchanged because the parsed record carried
|
||||
// `keyRaw: "codex_hooks"`. The caller's intent — emit the canonical
|
||||
// key — must win.
|
||||
rewritten += normalizeCodexHooksLine(match.text, key) + blockEol;
|
||||
cursor = blockEnd;
|
||||
return;
|
||||
}
|
||||
@@ -4612,11 +4632,17 @@ function ensureCodexHooksFeature(configContent) {
|
||||
record.end + record.eol.length <= featuresSection.end &&
|
||||
record.keySegments &&
|
||||
record.keySegments.length === 1 &&
|
||||
record.keySegments[0] === 'codex_hooks'
|
||||
isCodexHooksFeatureKey(record.keySegments[0])
|
||||
);
|
||||
|
||||
if (sectionLines.length > 0) {
|
||||
const rewritten = rewriteTomlKeyLines(configContent, sectionLines, 'codex_hooks');
|
||||
// Rewrite to canonical key — this migrates legacy `codex_hooks` to
|
||||
// `hooks` in-place on every reinstall. If the file already has the
|
||||
// canonical key the rewrite is a no-op shape-wise (same key, same
|
||||
// value). The rewriteTomlKeyLines helper preserves indentation,
|
||||
// trailing comments, and ownership-marker positioning, and always
|
||||
// emits the caller-supplied canonical key (#3566).
|
||||
const rewritten = rewriteTomlKeyLines(configContent, sectionLines, CODEX_HOOKS_FEATURE_KEY);
|
||||
return {
|
||||
content: repairTrappedFeaturesKeys(rewritten),
|
||||
ownership: null,
|
||||
@@ -4626,7 +4652,7 @@ function ensureCodexHooksFeature(configContent) {
|
||||
const sectionBody = configContent.slice(featuresSection.headerEnd, featuresSection.end);
|
||||
const needsSeparator = sectionBody.length > 0 && !sectionBody.endsWith('\n') && !sectionBody.endsWith('\r\n');
|
||||
const insertPrefix = sectionBody.length === 0 && featuresSection.headerEnd === configContent.length ? eol : '';
|
||||
const insertText = `${insertPrefix}${needsSeparator ? eol : ''}codex_hooks = true${eol}`;
|
||||
const insertText = `${insertPrefix}${needsSeparator ? eol : ''}${CODEX_HOOKS_FEATURE_KEY} = true${eol}`;
|
||||
const merged = configContent.slice(0, featuresSection.end) + insertText + configContent.slice(featuresSection.end);
|
||||
return {
|
||||
content: repairTrappedFeaturesKeys(merged),
|
||||
@@ -4644,11 +4670,11 @@ function ensureCodexHooksFeature(configContent) {
|
||||
);
|
||||
|
||||
const rootCodexHooksLines = rootFeatureLines
|
||||
.filter((record) => record.keySegments.length === 2 && record.keySegments[1] === 'codex_hooks');
|
||||
.filter((record) => record.keySegments.length === 2 && isCodexHooksFeatureKey(record.keySegments[1]));
|
||||
|
||||
if (rootCodexHooksLines.length > 0) {
|
||||
return {
|
||||
content: rewriteTomlKeyLines(configContent, rootCodexHooksLines, 'features.codex_hooks'),
|
||||
content: rewriteTomlKeyLines(configContent, rootCodexHooksLines, `features.${CODEX_HOOKS_FEATURE_KEY}`),
|
||||
ownership: null,
|
||||
};
|
||||
}
|
||||
@@ -4666,13 +4692,13 @@ function ensureCodexHooksFeature(configContent) {
|
||||
const prefix = insertAt > 0 && configContent[insertAt - 1] === '\n' ? '' : eol;
|
||||
return {
|
||||
content: configContent.slice(0, insertAt) +
|
||||
`${prefix}features.codex_hooks = true${eol}` +
|
||||
`${prefix}features.${CODEX_HOOKS_FEATURE_KEY} = true${eol}` +
|
||||
configContent.slice(insertAt),
|
||||
ownership: 'root_dotted',
|
||||
};
|
||||
}
|
||||
|
||||
const featuresBlock = `[features]${eol}codex_hooks = true${eol}`;
|
||||
const featuresBlock = `[features]${eol}${CODEX_HOOKS_FEATURE_KEY} = true${eol}`;
|
||||
if (!configContent) {
|
||||
return { content: featuresBlock, ownership: 'section' };
|
||||
}
|
||||
@@ -4703,11 +4729,11 @@ function hasEnabledCodexHooksFeature(configContent) {
|
||||
|
||||
const isSectionKey = record.tablePath === 'features' &&
|
||||
record.keySegments.length === 1 &&
|
||||
record.keySegments[0] === 'codex_hooks';
|
||||
isCodexHooksFeatureKey(record.keySegments[0]);
|
||||
const isRootDottedKey = record.tablePath === null &&
|
||||
record.keySegments.length === 2 &&
|
||||
record.keySegments[0] === 'features' &&
|
||||
record.keySegments[1] === 'codex_hooks';
|
||||
isCodexHooksFeatureKey(record.keySegments[1]);
|
||||
|
||||
if (!isSectionKey && !isRootDottedKey) {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user