Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to @rpamis/comet will be documented in this file.

## What's Changed [0.4.0-beta.16] - 2026-08-05

### Fixed

- **Classic dual-root recovery**: `comet classic root show` and migration dry-runs now remain available when both legacy and docs roots exist, exposing the conflict and a safe remediation path while keeping Classic writes blocked until it is resolved.
- **Codex Native Hook parsing**: Raw `apply_patch` input now attributes Add, Update, Delete, and standard `+++ b/...` file headers so Native phase protection is applied consistently.

## What's Changed [0.4.0-beta.15] - 2026-08-05

### Added
Expand Down
3 changes: 1 addition & 2 deletions app/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,7 @@ async function checkClassicLayout(projectPath: string): Promise<CheckResult> {
return {
check: 'Classic artifact layout',
status: 'fail',
message:
'both openspec/ and docs/openspec/ exist; Classic writes are blocked until the conflict is resolved',
message: `both ${path.relative(projectPath, inspection.paths.openSpecRoot).replaceAll('\\', '/')}/ and ${path.relative(projectPath, inspection.alternateRoot).replaceAll('\\', '/')}/ exist; Classic writes are blocked until the conflict is resolved — run comet classic root show, then comet classic root move docs --dry-run to inspect a safe migration; do not delete either root automatically`,
};
}
const configuredRoot = path
Expand Down
2 changes: 1 addition & 1 deletion assets/manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.4.0-beta.15",
"version": "0.4.0-beta.16",
"skills": [
"comet/SKILL.md",
"comet-classic/SKILL.md",
Expand Down
86 changes: 43 additions & 43 deletions assets/skills/comet-native/scripts/comet-native-hook-guard.mjs

Large diffs are not rendered by default.

178 changes: 89 additions & 89 deletions assets/skills/comet-native/scripts/comet-native-runtime.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion assets/skills/comet/scripts/comet-archive.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion assets/skills/comet/scripts/comet-guard.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion assets/skills/comet/scripts/comet-handoff.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion assets/skills/comet/scripts/comet-hook-guard.mjs

Large diffs are not rendered by default.

98 changes: 49 additions & 49 deletions assets/skills/comet/scripts/comet-hook-router.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion assets/skills/comet/scripts/comet-resume-probe.mjs

Large diffs are not rendered by default.

234 changes: 117 additions & 117 deletions assets/skills/comet/scripts/comet-runtime.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion assets/skills/comet/scripts/comet-state.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion assets/skills/comet/scripts/comet-yaml-validate.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion domains/comet-classic/classic-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class ClassicLayoutConflictError extends Error {
const configured = classicProjectRelative(projectRoot, configuredRoot);
const alternate = classicProjectRelative(projectRoot, alternateRoot);
super(
`Classic layout conflict: both ${configured}/ and ${alternate}/ exist; run comet doctor and resolve the conflict before writing`,
`Classic layout conflict: both ${configured}/ and ${alternate}/ exist; run comet classic root show, then comet classic root move docs --dry-run to inspect a safe migration before writing`,
);
this.name = 'ClassicLayoutConflictError';
}
Expand Down
14 changes: 12 additions & 2 deletions domains/comet-classic/classic-root-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
assertClassicLayoutReadable,
classicProjectRelative,
discoverClassicProject,
inspectClassicLayout,
type ClassicLayoutPaths,
} from './classic-layout.js';
import { readWorkflowProjectConfig } from '../workflow-contract/project-config-reader.js';
Expand Down Expand Up @@ -177,14 +178,21 @@ export const classicRootCommand: ClassicCommandHandler = async (args) => {
const [action, target, mode, ...extra] = args;
if (action === 'show' && target === undefined) {
const projectRoot = await discoverClassicProject(process.cwd());
const layout = await assertClassicLayoutReadable(projectRoot);
const layout = await assertClassicLayoutReadable(projectRoot, undefined, {
allowAlternateRoot: true,
});
const inspection = await inspectClassicLayout(projectRoot, layout.artifactLayout);
return {
exitCode: 0,
stdout:
JSON.stringify({
schema: 'comet.classic-layout.v1',
artifactLayout: layout.artifactLayout,
openSpecRoot: classicProjectRelative(projectRoot, layout.openSpecRoot),
alternateRoot: classicProjectRelative(projectRoot, inspection.alternateRoot),
configuredRootExists: inspection.configuredRootExists,
alternateRootExists: inspection.alternateRootExists,
dualRoots: inspection.dualRoots,
changesRoot: classicProjectRelative(projectRoot, layout.changesDir),
archiveRoot: classicProjectRelative(projectRoot, layout.archiveDir),
specsRoot: classicProjectRelative(projectRoot, layout.specsDir),
Expand All @@ -201,7 +209,9 @@ export const classicRootCommand: ClassicCommandHandler = async (args) => {
return usage(language);
}

const layout = await assertClassicLayoutReadable(projectRoot);
const layout = await assertClassicLayoutReadable(projectRoot, undefined, {
allowAlternateRoot: true,
});
Comment thread
benym marked this conversation as resolved.
Outdated
const reportMode: RootMoveMode = mode === '--dry-run' ? 'dry-run' : 'complete';
if (layout.artifactLayout === 'docs') {
return {
Expand Down
4 changes: 4 additions & 0 deletions domains/comet-entry/hook-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ export function parseCometHookRequest(source: string, filePath?: string): CometH
try {
input = JSON.parse(source) as unknown;
} catch {
const targets = patchTargets(source);
if (targets.length > 0) {
return { intent: 'write', targets: [...new Set(targets)], toolName: 'apply_patch' };
}
return { intent: 'unknown', targets: [], toolName: null };
}
if (!isRecord(input)) return { intent: 'unknown', targets: [], toolName: null };
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rpamis/comet",
"version": "0.4.0-beta.15",
"version": "0.4.0-beta.16",
"description": "Agent Skill Harness For Turning Ideas Into Evaluated Workflows",
"keywords": [
"comet",
Expand Down
2 changes: 1 addition & 1 deletion test/app/cli-help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('CLI help text', () => {
expect(help.status, help.stderr).toBe(0);
expect(help.stdout).toContain(tagline);
expect(packageJson.description).toBe(tagline);
expect(packageJson.version).toBe('0.4.0-beta.15');
expect(packageJson.version).toBe('0.4.0-beta.16');
});

it('marks bundle as the advanced backend and skill Engine runs as advanced', () => {
Expand Down
9 changes: 6 additions & 3 deletions test/app/doctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1108,9 +1108,12 @@ describe('doctor command', () => {
status: string;
message: string;
}>;
expect(results.find((result) => result.check === 'Classic artifact layout')).toMatchObject({
status: 'fail',
});
const layoutResult = results.find((result) => result.check === 'Classic artifact layout');
expect(layoutResult).toMatchObject({ status: 'fail' });
expect(layoutResult?.message).toContain('openspec/');
expect(layoutResult?.message).toContain('docs/openspec/');
expect(layoutResult?.message).toContain('comet classic root show');
expect(layoutResult?.message).toContain('comet classic root move docs --dry-run');
const stateResult = results.find((result) => result.check === '.comet.yaml: current-state');
expect(stateResult).toMatchObject({
status: 'pass',
Expand Down
39 changes: 36 additions & 3 deletions test/domains/comet-classic/classic-root-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,47 @@ describe('Classic root show', () => {
expect(result.stderr).toContain('Configured Classic OpenSpec root is missing');
});

it('fails when both roots exist', async () => {
it('reports both roots without blocking read-only root inspection', async () => {
await fs.mkdir(path.join(projectRoot, 'openspec'), { recursive: true });
await fs.mkdir(path.join(projectRoot, 'docs', 'openspec'), { recursive: true });

const result = await runClassicCli(['root', 'show']);

expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Classic layout conflict');
expect(result.exitCode).toBe(0);
expect(JSON.parse(result.stdout ?? '{}')).toMatchObject({
artifactLayout: 'legacy',
openSpecRoot: 'openspec',
alternateRoot: 'docs/openspec',
configuredRootExists: true,
alternateRootExists: true,
dualRoots: true,
});
});

it('exposes a safe dry-run path when both roots exist', async () => {
await fs.mkdir(path.join(projectRoot, 'openspec', 'changes', 'legacy'), {
recursive: true,
});
await fs.mkdir(path.join(projectRoot, 'docs', 'openspec', 'changes', 'docs'), {
recursive: true,
});

const result = await runClassicCli(['root', 'move', 'docs', '--dry-run']);

expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('目标初始状态: 非空目录');
expect(result.stdout).toContain('冲突:');
expect(result.stdout).toContain('docs 目标目录非空');
expect(result.stdout).toContain('请先解决上述冲突或阻塞项');

const configBefore = await fs.readFile(path.join(projectRoot, '.comet', 'config.yaml'), 'utf8');
const apply = await runClassicCli(['root', 'move', 'docs', '--apply']);

expect(apply.exitCode).toBe(70);
expect(apply.stderr).toContain('Classic 根目录迁移失败');
expect(await fs.readFile(path.join(projectRoot, '.comet', 'config.yaml'), 'utf8')).toBe(
configBefore,
);
});

it('fails when a managed root is a directory link', async () => {
Expand Down
26 changes: 26 additions & 0 deletions test/domains/comet-entry/hook-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,32 @@ describe('Comet Hook platform adapter', () => {
).toEqual({ intent: 'write', targets: ['src/b.ts'], toolName: 'apply_patch' });
});

it('normalizes raw Codex apply_patch input from Hook stdin', () => {
const patch = [
'*** Begin Patch',
'*** Update File: src/existing.ts',
'*** Add File: src/new.ts',
'*** Delete File: src/old.ts',
'*** End Patch',
].join('\n');

expect(parseCometHookRequest(patch)).toEqual({
intent: 'write',
targets: ['src/existing.ts', 'src/new.ts', 'src/old.ts'],
toolName: 'apply_patch',
});
});

it('normalizes standard unified-diff headers from raw patch input', () => {
const patch = ['--- a/src/old.ts', '+++ b/src/new.ts'].join('\n');

expect(parseCometHookRequest(patch)).toEqual({
intent: 'write',
targets: ['src/new.ts'],
toolName: 'apply_patch',
});
});

it('keeps an absolute Hook working directory for linked-worktree routing', () => {
const cwd = path.resolve('linked-worktree');
expect(
Expand Down
22 changes: 22 additions & 0 deletions test/domains/comet-entry/hook-router-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,26 @@ describe('packaged Hook Router worktree isolation', () => {
expect(primaryRequest.status).toBe(2);
expect(primaryRequest.stderr).toContain('primary-shape');
});

it('enforces Native Shape for raw Codex apply_patch input', async () => {
await configureChange(primary, 'raw-patch-shape', 'shape');
const patch = [
'*** Begin Patch',
'*** Update File: src/app.ts',
'@@',
'-old',
'+new',
'*** End Patch',
].join('\n');

const result = spawnSync(
process.execPath,
[router, '--platform', 'codex', '--project-root', primary],
{ cwd: primary, input: patch, encoding: 'utf8', timeout: 20_000 },
);

expect(result.status, result.stderr).toBe(2);
expect(result.stderr).toContain('raw-patch-shape');
expect(result.stderr).toContain('only allowed in build');
});
});
2 changes: 2 additions & 0 deletions test/domains/comet-entry/project-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ describe('Comet project status', () => {
changes: [],
error: expect.stringContaining('Classic layout conflict'),
});
expect(status.workflows.classic.error).toContain('comet classic root show');
expect(status.workflows.classic.error).toContain('comet classic root move docs --dry-run');
expect(status.unmanagedOpenSpec).toEqual([]);
});

Expand Down
2 changes: 1 addition & 1 deletion test/repository/release-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('release metadata', () => {
readFileSync(path.join(repositoryRoot, 'assets', 'manifest.json'), 'utf8'),
) as { version: string };

expect(packageJson.version).toBe('0.4.0-beta.15');
expect(packageJson.version).toBe('0.4.0-beta.16');
expect(packageLock.version).toBe(packageJson.version);
expect(packageLock.packages[''].version).toBe(packageJson.version);
expect(assetsManifest.version).toBe(packageJson.version);
Expand Down
Loading