fix: keep Ambient Resume for Classic-only projects - #338
Conversation
Reviewer's GuideThis PR decouples Ambient Resume instruction syncing from the Native workflow so Classic-only projects retain the managed instructions when ambient_resume is enabled, adds regression tests around init/update behavior for Classic-only projects, and updates release/version metadata and documentation accordingly. Sequence diagram for Ambient Resume syncing in init and update commandssequenceDiagram
actor User
participant initCommand
participant updateSingleProject
participant syncCometProjectInstructions
User->>initCommand: run comet init
initCommand->>syncCometProjectInstructions: syncCometProjectInstructions(projectPath, languageId, ambientResumeFlag)
alt [initialProjectConfigDocument.ambient_resume is defined]
syncCometProjectInstructions-->>initCommand: uses initialProjectConfigDocument.ambient_resume
else [initialProjectConfigDocument.ambient_resume is undefined]
syncCometProjectInstructions-->>initCommand: uses true (default)
end
User->>updateSingleProject: run comet update
updateSingleProject->>syncCometProjectInstructions: syncCometProjectInstructions(projectPath, projectLanguageId, ambientResumeFlag)
alt [projectConfigDocument.ambient_resume is defined]
syncCometProjectInstructions-->>updateSingleProject: uses projectConfigDocument.ambient_resume
else [projectConfigDocument.ambient_resume is undefined]
syncCometProjectInstructions-->>updateSingleProject: uses true (default)
end
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
👋 Thanks for opening your first PR to Comet, @mayzhaoyu. Before review, please make sure the PR title follows Conventional Commits, for example 🧪 The most useful local checks are: pnpm build
pnpm lint
pnpm format:check
pnpm test🧰 If your change touches ✨ We appreciate the contribution and will take a look as soon as we can. |
|
✅ PR template check passed. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (9)
Included review availability: Your plan includes up to 8 reviews per rolling hour; 7 remain after this review. 📝 WalkthroughWalkthroughAmbient Resume synchronization no longer depends on Native workflow selection. Classic-only ChangesAmbient Resume synchronization
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to The change is localized to preserving Ambient Resume instructions for Classic-only projects while retaining the disabled-setting behavior. No actionable merge-blocking risk remains. Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The new tests for Ambient Resume instructions duplicate the same string and assertion logic in multiple places (e.g., checking for
<comet-ambient-resume>andcomet resume-probe . --stdin --json); consider extracting a small helper to assert the ambient-resume block to keep the tests DRY and easier to update. - In the
updatetest you manually spy onconsole.logand aggregate JSON output; if a shared helper likecaptureJsonOutputis available (as used ininittests), reusing it here would simplify the test and keep the approach consistent across commands.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The new tests for Ambient Resume instructions duplicate the same string and assertion logic in multiple places (e.g., checking for `<comet-ambient-resume>` and `comet resume-probe . --stdin --json`); consider extracting a small helper to assert the ambient-resume block to keep the tests DRY and easier to update.
- In the `update` test you manually spy on `console.log` and aggregate JSON output; if a shared helper like `captureJsonOutput` is available (as used in `init` tests), reusing it here would simplify the test and keep the approach consistent across commands.
## Individual Comments
### Comment 1
<location path="test/app/update.test.ts" line_range="3529-3527" />
<code_context>
+ it('installs ambient resume instructions for Classic-only projects', async () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for Classic-only projects with `ambient_resume: false` in the update flow.
To fully cover this bug, please add a companion test for a Classic-only project with `ambient_resume: false` in the config. That test should assert that the managed ambient resume block is absent (or removed if previously inserted) while preserving existing `AGENTS.md`/`CLAUDE.md` content, analogous to the existing test for projects that disable the probe, but in the Classic-only case.
Suggested implementation:
```typescript
expect(claude).toContain('<comet-ambient-resume>');
});
it('does not install ambient resume instructions for Classic-only projects with ambient_resume disabled', async () => {
await arrangeClassicDocsOpenSpecUpdate(tmpDir, { ambientResume: false });
// Pre-existing user content plus a previously inserted ambient resume block
await fs.writeFile(
path.join(tmpDir, 'AGENTS.md'),
'# User\n\nKeep this.\n\n<comet-ambient-resume>\nPrevious content\n</comet-ambient-resume>\n',
'utf8',
);
await fs.writeFile(
path.join(tmpDir, 'CLAUDE.md'),
'# User\n\nAlso keep this.\n\n<comet-ambient-resume>\nPrevious content\n</comet-ambient-resume>\n',
'utf8',
);
const fakeHome = path.join(tmpDir, 'fake-home-classic-instructions-disabled');
const homedirSpy = vi.spyOn(os, 'homedir').mockReturnValue(fakeHome);
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
let json: string;
try {
await updateCommand(tmpDir, { json: true, skipNpm: true });
} finally {
homedirSpy.mockRestore();
log.mockRestore();
}
const agents = await fs.readFile(path.join(tmpDir, 'AGENTS.md'), 'utf8');
const claude = await fs.readFile(path.join(tmpDir, 'CLAUDE.md'), 'utf8');
// The managed ambient resume block should be absent
expect(agents).not.toContain('<comet-ambient-resume>');
expect(claude).not.toContain('<comet-ambient-resume>');
// User content should be preserved
expect(agents).toContain('# User\n\nKeep this.\n');
expect(claude).toContain('# User\n\nAlso keep this.\n');
});
it('installs ambient resume instructions for Classic-only projects', async () => {
```
To fully wire this up, ensure that Classic-only projects with `ambient_resume: false` are correctly arranged:
1. Update `arrangeClassicDocsOpenSpecUpdate` (or introduce a new helper) to accept an options object with `{ ambientResume: boolean }` and, when `ambientResume === false`, write the project config with `ambient_resume: false` in the Classic-only configuration.
2. If your existing "projects that disable the probe" test uses a different helper (e.g. `arrangeDocsOpenSpecUpdateWithAmbientResumeDisabled`), you can alternatively create a Classic-specific variant (e.g. `arrangeClassicDocsOpenSpecUpdateWithAmbientResumeDisabled(tmpDir)`) and call that from this new test instead of passing an options object—keep the test body the same, only change the helper invocation.
3. Confirm that any other tests calling `arrangeClassicDocsOpenSpecUpdate` are updated if you change its signature; if you add a new helper instead, no further changes should be required.
</issue_to_address>
### Comment 2
<location path="test/app/init-e2e.test.ts" line_range="509-507" />
<code_context>
).rejects.toMatchObject({ code: 'ENOENT' });
});
+ it('installs Ambient Resume instructions for Classic-only project init', async () => {
+ mockExternalSuccess();
+ await fs.mkdir(path.join(tmpDir, '.claude'), { recursive: true });
+ await fs.writeFile(path.join(tmpDir, 'AGENTS.md'), '# User\n\nKeep this.\n', 'utf8');
+ await fs.writeFile(path.join(tmpDir, 'CLAUDE.md'), '# User\n\nAlso keep this.\n', 'utf8');
+
+ const { initCommand } = await import('../../app/commands/init.js');
+ const result = await captureJsonOutput(() =>
+ initCommand(tmpDir, { yes: true, json: true, workflow: 'classic', language: 'en' }),
+ );
+
+ expect(result).toMatchObject({
+ workflow: 'classic',
+ initializedWorkflows: ['classic'],
+ });
+ const agents = await fs.readFile(path.join(tmpDir, 'AGENTS.md'), 'utf8');
+ const claude = await fs.readFile(path.join(tmpDir, 'CLAUDE.md'), 'utf8');
</code_context>
<issue_to_address>
**suggestion (testing):** Add a negative-path init E2E test for Classic-only projects with `ambient_resume: false`.
Please also add a negative-path E2E that initializes a Classic-only project with `ambient_resume: false` (or the probe disabled via config) and asserts that no `<comet-ambient-resume>` block is written to `AGENTS.md`/`CLAUDE.md` and existing user content is preserved. This will mirror the non-Classic removal behavior and verify that `ambient_resume: false` semantics remain unchanged.
Suggested implementation:
```typescript
it('installs Ambient Resume instructions for Classic-only project init', async () => {
mockExternalSuccess();
await fs.mkdir(path.join(tmpDir, '.claude'), { recursive: true });
await fs.writeFile(path.join(tmpDir, 'AGENTS.md'), '# User\n\nKeep this.\n', 'utf8');
await fs.writeFile(path.join(tmpDir, 'CLAUDE.md'), '# User\n\nAlso keep this.\n', 'utf8');
const { initCommand } = await import('../../app/commands/init.js');
const result = await captureJsonOutput(() =>
initCommand(tmpDir, { yes: true, json: true, workflow: 'classic', language: 'en' }),
);
expect(result).toMatchObject({
workflow: 'classic',
initializedWorkflows: ['classic'],
});
const agents = await fs.readFile(path.join(tmpDir, 'AGENTS.md'), 'utf8');
const claude = await fs.readFile(path.join(tmpDir, 'CLAUDE.md'), 'utf8');
for (const content of [agents, claude]) {
expect(content).toContain('<comet-ambient-resume>');
expect(content).toContain('comet resume-probe . --stdin --json');
}
expect(agents).toContain('# User\n\nKeep this.');
expect(claude).toContain('# User\n\nAlso keep this.');
});
it('does not install Ambient Resume instructions when Classic-only project has ambient_resume disabled', async () => {
mockExternalSuccess();
await fs.mkdir(path.join(tmpDir, '.claude'), { recursive: true });
await fs.writeFile(path.join(tmpDir, 'AGENTS.md'), '# User\n\nKeep this.\n', 'utf8');
await fs.writeFile(path.join(tmpDir, 'CLAUDE.md'), '# User\n\nAlso keep this.\n', 'utf8');
// Disable Ambient Resume via config for a Classic-only project
await fs.writeFile(
path.join(tmpDir, '.claude', 'config.json'),
JSON.stringify(
{
ambient_resume: false,
},
null,
2,
),
'utf8',
);
const { initCommand } = await import('../../app/commands/init.js');
const result = await captureJsonOutput(() =>
initCommand(tmpDir, { yes: true, json: true, workflow: 'classic', language: 'en' }),
);
expect(result).toMatchObject({
workflow: 'classic',
initializedWorkflows: ['classic'],
});
const agents = await fs.readFile(path.join(tmpDir, 'AGENTS.md'), 'utf8');
const claude = await fs.readFile(path.join(tmpDir, 'CLAUDE.md'), 'utf8');
for (const content of [agents, claude]) {
expect(content).not.toContain('<comet-ambient-resume>');
expect(content).not.toContain('comet resume-probe . --stdin --json');
}
expect(agents).toContain('# User\n\nKeep this.');
expect(claude).toContain('# User\n\nAlso keep this.');
});
it('adds Classic with the docs layout when a Native-only project is reinitialized as Both', async () => {
mockExternalSuccess();
```
If this repository uses a different config file name, location, or schema to disable Ambient Resume (e.g. `.claude/project.json`, YAML, or a nested `features: { ambient_resume: false }` object), adjust the `fs.writeFile` path and JSON structure in the new test to match the existing non-Classic negative-path Ambient Resume tests so that the init command actually observes the disabled setting.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| @@ -3526,6 +3526,36 @@ describe('update command helpers', () => { | |||
| expect(claude).toContain('<comet-ambient-resume>'); | |||
| }); | |||
There was a problem hiding this comment.
suggestion (testing): Add coverage for Classic-only projects with ambient_resume: false in the update flow.
To fully cover this bug, please add a companion test for a Classic-only project with ambient_resume: false in the config. That test should assert that the managed ambient resume block is absent (or removed if previously inserted) while preserving existing AGENTS.md/CLAUDE.md content, analogous to the existing test for projects that disable the probe, but in the Classic-only case.
Suggested implementation:
expect(claude).toContain('<comet-ambient-resume>');
});
it('does not install ambient resume instructions for Classic-only projects with ambient_resume disabled', async () => {
await arrangeClassicDocsOpenSpecUpdate(tmpDir, { ambientResume: false });
// Pre-existing user content plus a previously inserted ambient resume block
await fs.writeFile(
path.join(tmpDir, 'AGENTS.md'),
'# User\n\nKeep this.\n\n<comet-ambient-resume>\nPrevious content\n</comet-ambient-resume>\n',
'utf8',
);
await fs.writeFile(
path.join(tmpDir, 'CLAUDE.md'),
'# User\n\nAlso keep this.\n\n<comet-ambient-resume>\nPrevious content\n</comet-ambient-resume>\n',
'utf8',
);
const fakeHome = path.join(tmpDir, 'fake-home-classic-instructions-disabled');
const homedirSpy = vi.spyOn(os, 'homedir').mockReturnValue(fakeHome);
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
let json: string;
try {
await updateCommand(tmpDir, { json: true, skipNpm: true });
} finally {
homedirSpy.mockRestore();
log.mockRestore();
}
const agents = await fs.readFile(path.join(tmpDir, 'AGENTS.md'), 'utf8');
const claude = await fs.readFile(path.join(tmpDir, 'CLAUDE.md'), 'utf8');
// The managed ambient resume block should be absent
expect(agents).not.toContain('<comet-ambient-resume>');
expect(claude).not.toContain('<comet-ambient-resume>');
// User content should be preserved
expect(agents).toContain('# User\n\nKeep this.\n');
expect(claude).toContain('# User\n\nAlso keep this.\n');
});
it('installs ambient resume instructions for Classic-only projects', async () => {To fully wire this up, ensure that Classic-only projects with ambient_resume: false are correctly arranged:
- Update
arrangeClassicDocsOpenSpecUpdate(or introduce a new helper) to accept an options object with{ ambientResume: boolean }and, whenambientResume === false, write the project config withambient_resume: falsein the Classic-only configuration. - If your existing "projects that disable the probe" test uses a different helper (e.g.
arrangeDocsOpenSpecUpdateWithAmbientResumeDisabled), you can alternatively create a Classic-specific variant (e.g.arrangeClassicDocsOpenSpecUpdateWithAmbientResumeDisabled(tmpDir)) and call that from this new test instead of passing an options object—keep the test body the same, only change the helper invocation. - Confirm that any other tests calling
arrangeClassicDocsOpenSpecUpdateare updated if you change its signature; if you add a new helper instead, no further changes should be required.
| @@ -506,6 +506,31 @@ describe('comet init E2E', () => { | |||
| ).rejects.toMatchObject({ code: 'ENOENT' }); | |||
| }); | |||
There was a problem hiding this comment.
suggestion (testing): Add a negative-path init E2E test for Classic-only projects with ambient_resume: false.
Please also add a negative-path E2E that initializes a Classic-only project with ambient_resume: false (or the probe disabled via config) and asserts that no <comet-ambient-resume> block is written to AGENTS.md/CLAUDE.md and existing user content is preserved. This will mirror the non-Classic removal behavior and verify that ambient_resume: false semantics remain unchanged.
Suggested implementation:
it('installs Ambient Resume instructions for Classic-only project init', async () => {
mockExternalSuccess();
await fs.mkdir(path.join(tmpDir, '.claude'), { recursive: true });
await fs.writeFile(path.join(tmpDir, 'AGENTS.md'), '# User\n\nKeep this.\n', 'utf8');
await fs.writeFile(path.join(tmpDir, 'CLAUDE.md'), '# User\n\nAlso keep this.\n', 'utf8');
const { initCommand } = await import('../../app/commands/init.js');
const result = await captureJsonOutput(() =>
initCommand(tmpDir, { yes: true, json: true, workflow: 'classic', language: 'en' }),
);
expect(result).toMatchObject({
workflow: 'classic',
initializedWorkflows: ['classic'],
});
const agents = await fs.readFile(path.join(tmpDir, 'AGENTS.md'), 'utf8');
const claude = await fs.readFile(path.join(tmpDir, 'CLAUDE.md'), 'utf8');
for (const content of [agents, claude]) {
expect(content).toContain('<comet-ambient-resume>');
expect(content).toContain('comet resume-probe . --stdin --json');
}
expect(agents).toContain('# User\n\nKeep this.');
expect(claude).toContain('# User\n\nAlso keep this.');
});
it('does not install Ambient Resume instructions when Classic-only project has ambient_resume disabled', async () => {
mockExternalSuccess();
await fs.mkdir(path.join(tmpDir, '.claude'), { recursive: true });
await fs.writeFile(path.join(tmpDir, 'AGENTS.md'), '# User\n\nKeep this.\n', 'utf8');
await fs.writeFile(path.join(tmpDir, 'CLAUDE.md'), '# User\n\nAlso keep this.\n', 'utf8');
// Disable Ambient Resume via config for a Classic-only project
await fs.writeFile(
path.join(tmpDir, '.claude', 'config.json'),
JSON.stringify(
{
ambient_resume: false,
},
null,
2,
),
'utf8',
);
const { initCommand } = await import('../../app/commands/init.js');
const result = await captureJsonOutput(() =>
initCommand(tmpDir, { yes: true, json: true, workflow: 'classic', language: 'en' }),
);
expect(result).toMatchObject({
workflow: 'classic',
initializedWorkflows: ['classic'],
});
const agents = await fs.readFile(path.join(tmpDir, 'AGENTS.md'), 'utf8');
const claude = await fs.readFile(path.join(tmpDir, 'CLAUDE.md'), 'utf8');
for (const content of [agents, claude]) {
expect(content).not.toContain('<comet-ambient-resume>');
expect(content).not.toContain('comet resume-probe . --stdin --json');
}
expect(agents).toContain('# User\n\nKeep this.');
expect(claude).toContain('# User\n\nAlso keep this.');
});
it('adds Classic with the docs layout when a Native-only project is reinitialized as Both', async () => {
mockExternalSuccess();If this repository uses a different config file name, location, or schema to disable Ambient Resume (e.g. .claude/project.json, YAML, or a nested features: { ambient_resume: false } object), adjust the fs.writeFile path and JSON structure in the new test to match the existing non-Classic negative-path Ambient Resume tests so that the init command actually observes the disabled setting.
✨ Summary
Fixes #325.
comet initandcomet updateno longer tie Ambient Resume instruction syncing to the Native workflow. Classic-only projects now keep the managed block whenambient_resumeis enabled, whileambient_resume: falsestill removes it.Regression coverage was added for both commands, including preservation of existing content in
AGENTS.mdandCLAUDE.md.🎯 Scope
init,status,doctor,update)assets/skills/,assets/skills-zh/)assets/skills/comet/scripts/)🧪 Testing
pnpm buildpnpm lintpnpm run lint:architecturepnpm format:checkpnpm testpnpm test -- test/domains/comet-classic/comet-scripts.test.tsDependency installation could not complete locally because pnpm ran out of disk space (
ERR_PNPM_ENOSPC), so build, lint, and tests were not run locally.Additional checks:
git diff --check HEAD~1..HEADpackage.json,package-lock.json, andassets/manifest.json.✅ Checklist
fix: handle project-scope initREADME.md,README-zh.md, orCONTRIBUTING.mdCHANGELOG.mdis updated when behavior changesassets/manifest.jsonand relevant tests👀 Notes for Reviewers
README.mdandREADME-zh.mdalready document Ambient Resume as a shared setting for Native and Classic projects.ambient_resume: falsebehavior is unchanged.