Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/commands/__tests__/disable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ vi.mock('../../services/git.service.js', () => ({

import { gitService } from '../../services/git.service.js';
import { disableAction } from '../memory/disable.js';
import { MERGE_HOOK_MARKER, CODEX_NOTIFY_LINE } from '../memory/hooks.js';
import { MERGE_HOOK_MARKER, CODEX_NOTIFY_LINE, CODEX_NOTIFY_LINE_LEGACY } from '../memory/hooks.js';

let tmpDir: string;
let originalHome: string;
Expand Down Expand Up @@ -70,6 +70,18 @@ describe('disableAction', () => {
expect(content).toContain('model = "gpt-4"');
});

it('removes legacy Codex notify line', async () => {
const codexPath = path.join(tmpDir, '.codex', 'config.toml');
await fs.mkdir(path.dirname(codexPath), { recursive: true });
await fs.writeFile(codexPath, `model = "gpt-4"\n${CODEX_NOTIFY_LINE_LEGACY}\n`);

await disableAction();

const content = await fs.readFile(codexPath, 'utf-8');
expect(content).not.toContain(CODEX_NOTIFY_LINE_LEGACY);
expect(content).toContain('model = "gpt-4"');
});

it('removes kodus section from post-merge hook', async () => {
const hookPath = path.join(tmpDir, '.git', 'hooks', 'post-merge');
await fs.writeFile(hookPath, `#!/bin/sh\n${MERGE_HOOK_MARKER}\nif [ -n "$MERGED_BRANCH" ]; then\n kodus decisions promote --branch "$MERGED_BRANCH" &\nfi\n`, { mode: 0o755 });
Expand Down
14 changes: 13 additions & 1 deletion src/commands/__tests__/enable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ vi.mock('../../utils/module-matcher.js', () => ({

import { gitService } from '../../services/git.service.js';
import { enableAction } from '../memory/enable.js';
import { MERGE_HOOK_MARKER, CODEX_NOTIFY_LINE } from '../memory/hooks.js';
import { MERGE_HOOK_MARKER, CODEX_NOTIFY_LINE, CODEX_NOTIFY_LINE_LEGACY } from '../memory/hooks.js';

let tmpDir: string;

Expand Down Expand Up @@ -72,6 +72,18 @@ describe('enableAction', () => {
expect(calls).toContain('already configured');
});

it('migrates legacy codex notify line to stop event', async () => {
const codexConfig = path.join(tmpDir, '.codex', 'config.toml');
await fs.mkdir(path.dirname(codexConfig), { recursive: true });
await fs.writeFile(codexConfig, `${CODEX_NOTIFY_LINE_LEGACY}\n`, 'utf-8');

await enableAction({ codexConfig });

const content = await fs.readFile(codexConfig, 'utf-8');
expect(content).toContain(CODEX_NOTIFY_LINE);
expect(content).not.toContain(CODEX_NOTIFY_LINE_LEGACY);
});

it('--agents claude skips codex', async () => {
await enableAction({
agents: 'claude',
Expand Down
12 changes: 10 additions & 2 deletions src/commands/memory/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const CLAUDE_CAPTURE_COMMANDS = {
};

export const CODEX_NOTIFY_LINE =
'notify = ["kodus", "decisions", "capture", "--agent", "codex", "--event", "stop"]';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle legacy Codex notify line when switching events

Changing CODEX_NOTIFY_LINE to --event stop breaks upgrade behavior for users who already have the old --event agent-turn-complete line in ~/.codex/config.toml: installCodexNotify and removeCodexNotify both rely on exact-string matching via this constant, so after this change those existing installs are neither recognized as configured nor removable, and enable now skips updating them because the generic notify regex detects an existing entry. In practice, an upgraded user who reruns kodus decisions enable/disable can get stuck with the legacy notify command and no automatic migration path.

Useful? React with 👍 / 👎.

export const CODEX_NOTIFY_LINE_LEGACY =
'notify = ["kodus", "decisions", "capture", "--agent", "codex", "--event", "agent-turn-complete"]';

export const MERGE_HOOK_MARKER = '# kodus-memory-post-merge';
Expand Down Expand Up @@ -221,6 +223,12 @@ export async function installCodexNotify(configPath: string): Promise<{
return { configPath, changed: false, skipped: false, reason: '' };
}

if (content.includes(CODEX_NOTIFY_LINE_LEGACY)) {
const nextContent = content.replace(CODEX_NOTIFY_LINE_LEGACY, CODEX_NOTIFY_LINE);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug high

State corruption due to partial replacement in configuration file. The function installCodexNotify uses String.prototype.replace() to update a legacy configuration string. When the pattern is a string, replace() only affects the first occurrence. If a user's configuration file contains multiple instances of the legacy notification line (CODEX_NOTIFY_LINE_LEGACY), only the first one will be updated to the new version. All subsequent instances will remain, leaving the configuration file in a corrupted state with both old and new notification commands. This will cause unexpected behavior, such as duplicate command execution or configuration parsing errors.

    const nextContent = content.replaceAll(CODEX_NOTIFY_LINE_LEGACY, CODEX_NOTIFY_LINE);
Prompt for LLM

File src/commands/memory/hooks.ts:

Line 227:

I have a TypeScript function that reads a configuration file as a string, finds a legacy configuration line, and replaces it with a new one. The current implementation uses `String.prototype.replace()`. The problem is that `replace()` with a string argument only replaces the first match. If the legacy configuration line appears multiple times in the file, this code will only update the first one, leaving the file in a corrupted state with both old and new configuration lines. Please rewrite the line of code responsible for the replacement to ensure that all occurrences of the legacy configuration line are replaced with the new one.

Suggested Code:

    const nextContent = content.replaceAll(CODEX_NOTIFY_LINE_LEGACY, CODEX_NOTIFY_LINE);

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

await fs.writeFile(configPath, nextContent, 'utf-8');
return { configPath, changed: true, skipped: false, reason: '' };
}

if (notifyLinePattern.test(content)) {
return {
configPath,
Expand Down Expand Up @@ -365,13 +373,13 @@ export async function removeCodexNotify(configPath: string): Promise<{ configPat
return { configPath, removed: false };
}

if (!content.includes(CODEX_NOTIFY_LINE)) {
if (!content.includes(CODEX_NOTIFY_LINE) && !content.includes(CODEX_NOTIFY_LINE_LEGACY)) {
return { configPath, removed: false };
}

const nextContent = content
.split('\n')
.filter((line) => line.trim() !== CODEX_NOTIFY_LINE)
.filter((line) => line.trim() !== CODEX_NOTIFY_LINE && line.trim() !== CODEX_NOTIFY_LINE_LEGACY)
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.replace(/^\n+/, '')
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/cli.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ describe('decisions integration', () => {

const codexConfigPath = path.join(tmpHome, '.codex', 'config.toml');
const codexConfig = await fs.readFile(codexConfigPath, 'utf-8');
expect(codexConfig).toContain('notify = ["kodus", "decisions", "capture", "--agent", "codex", "--event", "agent-turn-complete"]');
expect(codexConfig).toContain('notify = ["kodus", "decisions", "capture", "--agent", "codex", "--event", "stop"]');

const hookPath = path.join(gitRepoDir, '.git', 'hooks', 'post-merge');
const hookContent = await fs.readFile(hookPath, 'utf-8');
Expand Down