Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
92c27e5
docs: design Codex hook path migration
benym Jul 13, 2026
c224fbc
docs: plan Codex hook path migration
benym Jul 13, 2026
a4b12ff
fix(codex): route hooks to hooks.json
benym Jul 13, 2026
3414fb0
fix(codex): migrate hook configuration
benym Jul 13, 2026
06b708a
fix(codex): preserve migrated hook groups
benym Jul 13, 2026
ca6fd3a
fix(codex): clean legacy hook files
benym Jul 13, 2026
935e674
fix(codex): isolate hook cleanup failures
benym Jul 13, 2026
20c7927
docs: record Codex hook path fix
benym Jul 13, 2026
28bdbf2
chore: bump version to 0.4.0-beta.5
benym Jul 13, 2026
f87c170
fix(codex): harden hook migration
benym Jul 13, 2026
ae0f5c9
docs: format Codex hook migration plan
benym Jul 13, 2026
4a95629
docs: remove implementation planning files
benym Jul 13, 2026
74bc4f1
docs: design standard Superpowers artifact routing
benym Jul 13, 2026
0b2e86f
docs: add standard artifact routing plan
benym Jul 13, 2026
424e25d
fix: drive comet-open from OpenSpec status
benym Jul 13, 2026
dc6890d
fix(classic): allow standard Superpowers artifact writes
benym Jul 13, 2026
07bd60f
fix(classic): harden Superpowers artifact slot routing
benym Jul 13, 2026
02d3e75
docs: document standard Superpowers artifact routing
benym Jul 13, 2026
051dcb3
fix(classic): close Superpowers artifact routing bypasses
benym Jul 13, 2026
262ef0d
test(classic): validate migration guard outcomes
benym Jul 13, 2026
cd89a88
docs: remove unless file
benym Jul 13, 2026
6790ac3
fix(codex): isolate malformed legacy hook configs
benym Jul 13, 2026
2363b04
fix(codex): make legacy hook cleanup best effort
benym Jul 13, 2026
54dcfdd
refactor(skill): align hook cleanup test ownership
benym Jul 13, 2026
d3e6a44
docs: design Skill Hook Rule lifecycle hardening
benym Jul 13, 2026
35cb6c0
docs: plan Skill Hook Rule lifecycle hardening
benym Jul 13, 2026
bafb3dd
docs: refine lifecycle inspection boundaries
benym Jul 13, 2026
aed9dea
fix(skill): fail closed on invalid Hook configuration
benym Jul 13, 2026
4e284d2
fix(skill): harden Hook failure boundaries
benym Jul 13, 2026
d46560f
fix(skill): propagate Rule and removal failures
benym Jul 13, 2026
b148669
fix(skill): surface lifecycle inspection failures
benym Jul 13, 2026
640ef1d
fix(skill): preserve legacy and Kiro Hook boundaries
benym Jul 13, 2026
ea3c683
docs: align lifecycle plan with runtime sync
benym Jul 13, 2026
6794d52
fix(skill): surface incomplete Comet lifecycle updates
benym Jul 13, 2026
47f97e6
fix(skill): preserve lifecycle target failure details
benym Jul 13, 2026
bb2cbbd
feat(doctor): detect incomplete Rule and Hook installs
benym Jul 13, 2026
24c8449
fix(doctor): deduplicate shared Skill roots
benym Jul 13, 2026
2ac0497
docs: document Skill lifecycle integrity fixes
benym Jul 13, 2026
81bff08
fix(skill): harden lifecycle ownership and cleanup
benym Jul 13, 2026
f811178
fix(skill): complete lifecycle recovery safeguards
benym Jul 13, 2026
8732501
fix(skill): retry registered platform cleanup
benym Jul 13, 2026
53a26c9
fix(skill): merge detected and recovery cleanup targets
benym Jul 13, 2026
7c3f3d2
fix: tighten comet workflow guardrails
benym Jul 14, 2026
1e9ae57
test: update classic contract projection
benym Jul 14, 2026
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

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

## What's Changed [0.4.0-beta.5] - 2026-07-14

### Changed

- **Skill trigger and decision authoring**: Built-in phase Skills and Creator-generated internal Node Skills now declare explicit entry/runtime boundaries, while Creator templates classify automatic handling, stop conditions, and manual handoffs before emitting user pauses. This prevents ordinary tasks, guard failures, capability gaps, and single-option recovery paths from invoking internal phases or prompting unnecessarily.
- **Comet workflow checkpoints**: Clear requests now skip redundant pre-artifact naming confirmation, Build preflights executable capabilities and combines adjacent configuration choices into one decision, and manual handoffs return control without asking again. Full workflows also initialize recoverable state before artifact generation, persist large-PRD batch manifests, and keep resumability independent of unwritten conversation state.
- **Verification repair loops and archive ownership**: Verification now automatically returns the first three actionable failures to Build, persists the consecutive failure count across resumes, pauses only for real tradeoffs or retry-limit decisions, and keeps CRITICAL and IMPORTANT findings non-waivable. Verification records evidence without finishing the branch, while archive commits only attributed paths before branch handling so the final branch or PR includes merged specs and archive metadata.
- **Preset execution semantics**: Hotfix and tweak workflows now record truthful current-workspace isolation, retain regression testing in direct mode, avoid escalating on task count alone, and discard lightweight execution settings when upgraded to the full workflow.

### Fixed

- **OpenSpec workflow compatibility**: Comet now requires OpenSpec 1.5 or newer, reports incompatible installations in setup and Doctor, drives `/comet-open` from `applyRequires` and the live schema, validates repository-local paths and concrete outputs, and resumes persisted split batches without recreating completed changes.
- **Codex hook configuration**: Project and global Codex installs now write phase guard hooks to the supported `.codex/hooks.json` location and safely migrate Comet-managed entries from the previously generated `settings.local.json` without changing user-defined hooks or settings ([#199](https://github.com/rpamis/comet/issues/199)).
- **Standard Superpowers artifacts**: Classic write hooks now accept first-time design, plan, and verification artifacts in their standard workflow directories without requiring Comet-specific filename suffixes, while selected-change, phase, and occupied-slot checks still prevent ambiguous or duplicate writes.
- **Skill lifecycle integrity**: Comet now preserves malformed user Hook configuration, reports Skill, Rule, and Hook failures consistently across init, update, Doctor, and uninstall, and avoids registering partial installations as complete.

## What's Changed [0.4.0-beta.4] - 2026-07-11

### Added
Expand Down
101 changes: 88 additions & 13 deletions app/commands/doctor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import path from 'path';
import os from 'os';
import { execSync } from 'child_process';
import { fileExists, readDir } from '../../platform/fs/file-system.js';
import { isCommandAvailable } from '../../domains/integrations/openspec.js';
import {
getOpenSpecVersion,
isCommandAvailable,
isOpenSpecVersionCompatible,
MINIMUM_OPENSPEC_VERSION,
} from '../../domains/integrations/openspec.js';
import {
hasCodegraphProjectIndex,
resolveCodegraphCommand,
Expand All @@ -12,8 +16,16 @@ import {
getAssetsDir,
getManagedSkillPaths,
} from '../../domains/skill/platform-install.js';
import { PLATFORMS, getPlatformSkillsDirs } from '../../platform/install/platforms.js';
import { hasPlatformDetectionPath } from '../../platform/install/detect.js';
import {
getPlatformRuleDestinations,
inspectCometHooksForPlatform,
} from '../../domains/skill/platform-inspect.js';
import {
PLATFORMS,
getPlatformSkillsDirs,
type Platform,
} from '../../platform/install/platforms.js';
import { resolveCanonicalSkillRootOwners } from '../../platform/install/skill-root-owner.js';
import type { InstallScope } from '../../platform/install/types.js';
import { inspectClassicChange } from '../../domains/comet-classic/classic-diagnostics.js';
import { getCurrentVersion } from '../../platform/version/version.js';
Expand Down Expand Up @@ -51,14 +63,15 @@ async function checkOpenSpecCli(): Promise<CheckResult> {
message: 'not installed — install with: npm install -g @fission-ai/openspec@latest',
};
}
try {
const version = execSync('openspec --version', { stdio: 'pipe', timeout: 10_000 })
.toString()
.trim();
return { check: 'openspec CLI', status: 'pass', message: `installed (${version})` };
} catch {
return { check: 'openspec CLI', status: 'pass', message: 'installed' };
const version = getOpenSpecVersion();
if (!version || !isOpenSpecVersionCompatible(version)) {
return {
check: 'openspec CLI',
status: 'warn',
message: `installed (${version || 'version unknown'}), but Comet requires >= ${MINIMUM_OPENSPEC_VERSION} — run: npm install -g @fission-ai/openspec@latest`,
};
}
return { check: 'openspec CLI', status: 'pass', message: `installed (${version})` };
}

function checkEnvironment(projectPath: string, context: DoctorContext): CheckResult {
Expand Down Expand Up @@ -167,6 +180,65 @@ function getScopeBases(
return bases;
}

async function checkPlatformComponents(
baseDir: string,
platform: (typeof PLATFORMS)[number],
scope: InstallScope,
): Promise<CheckResult[]> {
const results: CheckResult[] = [];
const ruleDestinations = await getPlatformRuleDestinations(baseDir, platform, scope);
if (ruleDestinations.length > 0) {
let present = 0;
const inspectionErrors: string[] = [];
for (const destination of ruleDestinations) {
try {
if (await fileExists(destination)) present++;
} catch (error) {
inspectionErrors.push(`${destination}: ${(error as Error).message}`);
}
}
results.push({
check: `rules: ${platform.name} (${scope})`,
status:
inspectionErrors.length === 0 && present === ruleDestinations.length ? 'pass' : 'warn',
message:
inspectionErrors.length > 0
? `unable to inspect managed Rule (${inspectionErrors.join('; ')}) — run: comet update --scope ${scope}`
: present === ruleDestinations.length
? `complete (${present} files)`
: `partial (${present}/${ruleDestinations.length} files) — run: comet update --scope ${scope}`,
});
}

if (platform.supportsHooks && platform.hookFormat) {
const inspection = await inspectCometHooksForPlatform(baseDir, platform, scope);
results.push({
check: `hooks: ${platform.name} (${scope})`,
status: inspection.present ? 'pass' : 'warn',
message: inspection.present
? 'managed Hook present'
: `${inspection.error ?? 'managed Hook missing'} — run: comet update --scope ${scope}`,
});
}

return results;
}

async function getPlatformsForSkillInspection(
baseDir: string,
scope: InstallScope,
doctorScope: DoctorScope,
): Promise<Array<{ platform: Platform; inspectComponents: boolean }>> {
return (
await resolveCanonicalSkillRootOwners(baseDir, scope, {
respectDetectionPaths: doctorScope === 'auto',
})
).map(({ platform, hasOwnershipEvidence, sharedCanonicalRoot }) => ({
platform,
inspectComponents: !sharedCanonicalRoot || hasOwnershipEvidence,
}));
}

async function checkSkillCompleteness(
projectPath: string,
scope: DoctorScope,
Expand All @@ -183,8 +255,8 @@ async function checkSkillCompleteness(
global: { hasInstall: false, hasComplete: false },
};
for (const base of getScopeBases(projectPath, scope, context)) {
for (const platform of PLATFORMS) {
if (scope === 'auto' && !(await hasPlatformDetectionPath(base.baseDir, platform))) continue;
const platforms = await getPlatformsForSkillInspection(base.baseDir, base.scope, scope);
for (const { platform, inspectComponents } of platforms) {
const skillsDirs = getPlatformSkillsDirs(platform, base.scope);
const canonicalSkillsDir = skillsDirs[0];
let detectedSkillsDir: string | undefined;
Expand Down Expand Up @@ -232,6 +304,9 @@ async function checkSkillCompleteness(
message: `partial (${present.length}/${total} files; missing ${missing.length}) — run: comet update --scope ${base.scope}`,
},
);
if (inspectComponents) {
results.push(...(await checkPlatformComponents(base.baseDir, platform, base.scope)));
}
}
}

Expand Down
104 changes: 79 additions & 25 deletions app/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type InitOptions = {
};

type InstallStatus = 'installed' | 'skipped' | 'failed';
type ComponentAction = 'overwrite' | 'skip' | 'install';
type ComponentAction = 'overwrite' | 'skip' | 'install' | 'reuse';
type BulkOverwriteChoice = 'overwrite-all' | 'skip-all' | 'choose';

interface PlatformResult {
Expand Down Expand Up @@ -186,6 +186,11 @@ function resolveAction(
return 'install';
}

function resolveCometAction(hasExisting: boolean, options: InitOptions): ComponentAction {
if (hasExisting && options.yes && !options.overwrite && !options.skipExisting) return 'reuse';
return resolveAction(hasExisting, options);
}

type NpmDepId = 'openspec' | 'superpowers' | 'codegraph';

interface NpmDepState {
Expand Down Expand Up @@ -385,7 +390,7 @@ export async function initCommand(targetPath: string, options: InitOptions = {})

let osAction = resolveAction(hasOS, options);
let spAction = resolveAction(hasSP, options);
let cmAction = resolveAction(hasCM, options);
let cmAction = resolveCometAction(hasCM, options);

if (!options.yes) {
const existingComponents = [
Expand Down Expand Up @@ -488,6 +493,8 @@ export async function initCommand(targetPath: string, options: InitOptions = {})
: `${scope === 'global' ? '~/' : ''}${platformSkillsDir}/skills/`;

let cmStatus: InstallStatus = 'skipped';
let cometComponentInstalled = false;
let skillFailed = false;
if (cmAction !== 'skip') {
const { copied, failed } = await copyCometSkillsForPlatform(
baseDir,
Expand All @@ -497,38 +504,71 @@ export async function initCommand(targetPath: string, options: InitOptions = {})
scope,
installMode,
);
skillFailed = failed > 0;
cmStatus = failed > 0 ? 'failed' : copied > 0 ? 'installed' : 'skipped';
log(
` Comet -> ${platform.name}: ${cmStatus} (${copied} files${
failed > 0 ? `, ${failed} failed` : ''
}) -> ${skillsPath}`,
);
cometComponentInstalled = copied > 0;
if (cmAction === 'reuse' && copied === 0 && failed === 0) {
log(` Comet -> ${platform.name}: reused (${t(lang, 'alreadyExists')})`);
} else {
log(
` Comet -> ${platform.name}: ${cmStatus} (${copied} files${
failed > 0 ? `, ${failed} failed` : ''
}) -> ${skillsPath}`,
);
}
} else {
log(` Comet -> ${platform.name}: skipped (${t(lang, 'alreadyExists')})`);
}

if (cmAction !== 'skip') {
const { copied: ruleCopied } = await copyCometRulesForPlatform(
baseDir,
platform,
cmAction === 'overwrite',
language.id,
scope,
);
if (ruleCopied > 0) {
log(` Comet rules -> ${platform.name}: ${ruleCopied} ${t(lang, 'rulesInstalled')}`);
if (cmAction !== 'skip' && !skillFailed) {
try {
const { copied: ruleCopied, failed: ruleFailed } = await copyCometRulesForPlatform(
baseDir,
platform,
cmAction === 'overwrite',
language.id,
scope,
);
cometComponentInstalled ||= ruleCopied > 0;
if (ruleCopied > 0) {
log(` Comet rules -> ${platform.name}: ${ruleCopied} ${t(lang, 'rulesInstalled')}`);
}
if (ruleFailed > 0) {
cmStatus = 'failed';
log(` Comet rules -> ${platform.name}: ${t(lang, 'rulesFailed')} (${ruleFailed})`);
}
} catch (err) {
cmStatus = 'failed';
log(
` Comet rules -> ${platform.name}: ${t(lang, 'rulesFailed')} (${(err as Error).message})`,
);
}
}

if (cmAction !== 'skip' && platform.supportsHooks) {
const { installed, reason } = await installCometHooksForPlatform(baseDir, platform, scope);
if (installed) {
log(` Comet hooks -> ${platform.name}: ${t(lang, 'hooksInstalled')}`);
} else if (reason) {
log(` Comet hooks -> ${platform.name}: ${t(lang, 'hooksSkipped')} (${reason})`);
if (cmAction !== 'skip' && !skillFailed) {
try {
const { status, reason } = await installCometHooksForPlatform(baseDir, platform, scope);
cometComponentInstalled ||= status === 'installed';
if (status === 'installed') {
log(` Comet hooks -> ${platform.name}: ${t(lang, 'hooksInstalled')}`);
} else if (status === 'failed') {
cmStatus = 'failed';
log(` Comet hooks -> ${platform.name}: ${t(lang, 'hooksFailed')} (${reason})`);
} else if (reason && platform.supportsHooks) {
log(` Comet hooks -> ${platform.name}: ${t(lang, 'hooksSkipped')} (${reason})`);
}
} catch (err) {
cmStatus = 'failed';
log(
` Comet hooks -> ${platform.name}: ${t(lang, 'hooksFailed')} (${(err as Error).message})`,
);
}
}

if (cmAction !== 'skip' && cmStatus !== 'failed') {
cmStatus = cometComponentInstalled ? 'installed' : 'skipped';
}

results.push({
platform,
openspec: osToolIds.includes(platform.openspecToolId) ? osGlobalStatus : 'skipped',
Expand Down Expand Up @@ -562,10 +602,24 @@ export async function initCommand(targetPath: string, options: InitOptions = {})
if (scope === 'project') {
await createWorkingDirs(projectPath, language.artifactLanguage);
const projectTargets = await detectInstalledCometTargets(projectPath, { scopes: ['project'] });
if (projectTargets.length > 0) {
const successfulCometPlatforms = new Set(
results
.filter(
(result) =>
result.comet !== 'failed' &&
plans.some(
(plan) => plan.platform.id === result.platform.id && plan.cmAction !== 'skip',
),
)
.map((result) => result.platform.id),
);
const completeProjectTargets = projectTargets.filter((target) =>
successfulCometPlatforms.has(target.platform.id),
);
if (completeProjectTargets.length > 0) {
await upsertProjectInstallation(
projectPath,
projectTargets.map((target) => ({
completeProjectTargets.map((target) => ({
platform: target.platform.id,
language: target.language,
})),
Expand Down
Loading
Loading