chore: batch update - cleanup ghost commands, ccw-help index refresh, CLI session/orchestrator enhancements, skill minor fixes

- Add cleanup-ghost-commands.mjs script
- Refresh ccw-help index files (remove stale entries)
- CLI session manager: add instruction assembler and launch registry
- Frontend: orchestrator plan builder, property panel, dashboard toolbar updates
- Flow executor and type updates
- Minor fixes across multiple skills and commands
This commit is contained in:
catlog22
2026-02-17 21:53:51 +08:00
parent 1f53f2de27
commit 357f48a0c3
45 changed files with 751 additions and 1643 deletions

View File

@@ -0,0 +1,402 @@
#!/usr/bin/env node
/**
* Cleanup script: Remove ghost command entries from ccw-help JSON indexes
* and fix broken slash command references in skill/command .md files.
*
* Usage: node cleanup-ghost-commands.mjs [--dry-run]
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { glob } from 'fs/promises';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const COMMANDS_DIR = path.join(ROOT, 'commands');
const SKILLS_DIR = path.join(ROOT, 'skills');
const INDEX_DIR = path.join(SKILLS_DIR, 'ccw-help', 'index');
const DRY_RUN = process.argv.includes('--dry-run');
const log = (msg) => console.log(DRY_RUN ? `[DRY-RUN] ${msg}` : msg);
// ─── Step 1: Build set of existing command source files ───
function getExistingCommandSources() {
const sources = new Set();
function walk(dir, prefix = '') {
if (!fs.existsSync(dir)) return;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
walk(path.join(dir, entry.name), rel);
} else if (entry.name.endsWith('.md')) {
// Store as relative path from index dir: ../../../commands/...
sources.add(`../../../commands/${rel}`);
}
}
}
walk(COMMANDS_DIR);
return sources;
}
// ─── Step 2: Build mapping of old commands → new skill names ───
// These commands were migrated to skills but references were never updated
const COMMAND_TO_SKILL_MAP = {
// workflow commands → skills
'/workflow:plan': 'workflow-plan',
'/workflow:execute': 'workflow-execute',
'/workflow:lite-plan': 'workflow-lite-plan',
'/workflow:lite-execute': 'workflow-lite-plan', // lite-execute is part of lite-plan skill
'/workflow:lite-fix': 'workflow-lite-plan', // lite-fix is part of lite-plan skill
'/workflow:multi-cli-plan': 'workflow-multi-cli-plan',
'/workflow:plan-verify': 'workflow-plan', // plan-verify is a phase of workflow-plan
'/workflow:replan': 'workflow-plan', // replan is a phase of workflow-plan
'/workflow:tdd-plan': 'workflow-tdd',
'/workflow:tdd-verify': 'workflow-tdd', // tdd-verify is a phase of workflow-tdd
'/workflow:test-fix-gen': 'workflow-test-fix',
'/workflow:test-gen': 'workflow-test-fix',
'/workflow:test-cycle-execute': 'workflow-test-fix',
'/workflow:review': 'review-cycle',
'/workflow:review-session-cycle': 'review-cycle',
'/workflow:review-module-cycle': 'review-cycle',
'/workflow:review-cycle-fix': 'review-cycle',
'/workflow:status': 'workflow-execute', // status is part of workflow-execute
// brainstorm commands → skills
'/workflow:brainstorm:artifacts': 'brainstorm',
'/workflow:brainstorm:auto-parallel': 'brainstorm',
'/workflow:brainstorm:role-analysis': 'brainstorm',
'/workflow:brainstorm:synthesis': 'brainstorm',
// tools commands → skill phases
'/workflow:tools:context-gather': 'workflow-plan',
'/workflow:tools:conflict-resolution': 'workflow-plan',
'/workflow:tools:task-generate-agent': 'workflow-plan',
'/workflow:tools:task-generate-tdd': 'workflow-tdd',
'/workflow:tools:tdd-coverage-analysis': 'workflow-tdd',
'/workflow:tools:test-concept-enhanced': 'workflow-test-fix',
'/workflow:tools:test-context-gather': 'workflow-test-fix',
'/workflow:tools:test-task-generate': 'workflow-test-fix',
// memory commands → skills
'/memory:compact': 'memory-capture',
'/memory:tips': 'memory-capture',
'/memory:load': 'memory-manage',
'/memory:docs': 'memory-manage',
'/memory:docs-full-cli': 'memory-manage',
'/memory:docs-related-cli': 'memory-manage',
'/memory:update-full': 'memory-manage',
'/memory:update-related': 'memory-manage',
// general commands
'/ccw-debug': null, // deleted, no replacement
'/ccw view': null, // deleted, no replacement
'/workflow:lite-lite-lite': 'workflow-lite-plan',
// ui-design (these still exist as commands)
'/workflow:ui-design:auto': '/workflow:ui-design:explore-auto',
'/workflow:ui-design:update': '/workflow:ui-design:generate',
};
// ─── Step 3: Clean JSON index files ───
function cleanAllCommandsJson(existingSources) {
const filePath = path.join(INDEX_DIR, 'all-commands.json');
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
const before = data.length;
const cleaned = data.filter(entry => {
const exists = existingSources.has(entry.source);
if (!exists) {
log(` REMOVE from all-commands.json: ${entry.command} (source: ${entry.source})`);
}
return exists;
});
const removed = before - cleaned.length;
if (removed > 0) {
if (!DRY_RUN) fs.writeFileSync(filePath, JSON.stringify(cleaned, null, 2) + '\n');
log(` all-commands.json: removed ${removed} ghost entries (${before}${cleaned.length})`);
} else {
log(` all-commands.json: no ghost entries found`);
}
return removed;
}
function cleanEssentialCommandsJson(existingSources) {
const filePath = path.join(INDEX_DIR, 'essential-commands.json');
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
const before = data.length;
const cleaned = data.filter(entry => {
const exists = existingSources.has(entry.source);
if (!exists) {
log(` REMOVE from essential-commands.json: ${entry.command} (source: ${entry.source})`);
}
return exists;
});
const removed = before - cleaned.length;
if (removed > 0) {
if (!DRY_RUN) fs.writeFileSync(filePath, JSON.stringify(cleaned, null, 2) + '\n');
log(` essential-commands.json: removed ${removed} ghost entries`);
}
return removed;
}
function cleanByCategoryJson(existingSources) {
const filePath = path.join(INDEX_DIR, 'by-category.json');
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
let totalRemoved = 0;
for (const [category, subcats] of Object.entries(data)) {
for (const [subcat, entries] of Object.entries(subcats)) {
if (!Array.isArray(entries)) continue;
const before = entries.length;
const cleaned = entries.filter(entry => {
const exists = existingSources.has(entry.source);
if (!exists) {
log(` REMOVE from by-category.json[${category}][${subcat}]: ${entry.command}`);
}
return exists;
});
if (cleaned.length < before) {
subcats[subcat] = cleaned;
totalRemoved += before - cleaned.length;
}
// Remove empty subcategory arrays
if (cleaned.length === 0) {
delete subcats[subcat];
}
}
// Remove empty categories
if (Object.keys(subcats).length === 0) {
delete data[category];
}
}
if (totalRemoved > 0) {
if (!DRY_RUN) fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
log(` by-category.json: removed ${totalRemoved} ghost entries`);
}
return totalRemoved;
}
function cleanByUseCaseJson(existingSources) {
const filePath = path.join(INDEX_DIR, 'by-use-case.json');
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
let totalRemoved = 0;
for (const [useCase, entries] of Object.entries(data)) {
if (!Array.isArray(entries)) continue;
const before = entries.length;
const cleaned = entries.filter(entry => {
const exists = existingSources.has(entry.source);
if (!exists) {
log(` REMOVE from by-use-case.json[${useCase}]: ${entry.command}`);
}
return exists;
});
if (cleaned.length < before) {
data[useCase] = cleaned;
totalRemoved += before - cleaned.length;
}
if (cleaned.length === 0) {
delete data[useCase];
}
}
if (totalRemoved > 0) {
if (!DRY_RUN) fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
log(` by-use-case.json: removed ${totalRemoved} ghost entries`);
}
return totalRemoved;
}
function cleanCommandRelationshipsJson(existingSources) {
const filePath = path.join(INDEX_DIR, 'command-relationships.json');
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
// Build set of existing command names (without leading /)
// From source paths like ../../../commands/workflow/session/start.md → workflow:session:start
const existingCommands = new Set();
for (const src of existingSources) {
// ../../../commands/workflow/session/start.md → workflow/session/start
const rel = src.replace('../../../commands/', '').replace('.md', '');
// workflow/session/start → workflow:session:start
const cmd = rel.replace(/\//g, ':');
existingCommands.add(cmd);
}
// Also add skill names as valid targets
if (fs.existsSync(SKILLS_DIR)) {
for (const entry of fs.readdirSync(SKILLS_DIR, { withFileTypes: true })) {
if (entry.isDirectory()) existingCommands.add(entry.name);
}
}
// Remove top-level keys that reference non-existent commands
const keysToRemove = [];
for (const key of Object.keys(data)) {
if (!existingCommands.has(key)) {
keysToRemove.push(key);
}
}
// Also clean internal references
for (const [key, relations] of Object.entries(data)) {
for (const [relType, refs] of Object.entries(relations)) {
if (Array.isArray(refs)) {
const cleaned = refs.filter(ref => existingCommands.has(ref));
if (cleaned.length < refs.length) {
log(` CLEAN command-relationships.json[${key}][${relType}]: removed ${refs.length - cleaned.length} dead refs`);
relations[relType] = cleaned;
}
}
}
}
for (const key of keysToRemove) {
log(` REMOVE command-relationships.json key: ${key}`);
delete data[key];
}
if (keysToRemove.length > 0) {
if (!DRY_RUN) fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
log(` command-relationships.json: removed ${keysToRemove.length} ghost keys`);
}
return keysToRemove.length;
}
// ─── Step 4: Fix broken references in .md files ───
function findMdFiles(dir) {
const results = [];
if (!fs.existsSync(dir)) return results;
function walk(d) {
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
const full = path.join(d, entry.name);
if (entry.isDirectory()) {
// Skip _shared, index dirs
if (entry.name === 'node_modules' || entry.name === 'index') continue;
walk(full);
} else if (entry.name.endsWith('.md')) {
results.push(full);
}
}
}
walk(dir);
return results;
}
function fixBrokenReferences() {
const mdFiles = [
...findMdFiles(COMMANDS_DIR),
...findMdFiles(SKILLS_DIR),
];
let totalFixes = 0;
const fixLog = [];
for (const filePath of mdFiles) {
let content = fs.readFileSync(filePath, 'utf8');
let modified = false;
const relPath = path.relative(ROOT, filePath);
// Fix Skill() invocations with wrong skill names
// e.g., Skill(skill="compact" → Skill(skill="memory-capture"
const skillCallFixes = {
'skill="compact"': 'skill="memory-capture"',
"skill='compact'": "skill='memory-capture'",
'skill="workflow:brainstorm:role-analysis"': 'skill="brainstorm"',
"skill='workflow:brainstorm:role-analysis'": "skill='brainstorm'",
'skill="workflow:lite-execute"': 'skill="workflow-lite-plan"',
"skill='workflow:lite-execute'": "skill='workflow-lite-plan'",
};
for (const [oldCall, newCall] of Object.entries(skillCallFixes)) {
if (content.includes(oldCall)) {
content = content.replaceAll(oldCall, newCall);
modified = true;
fixLog.push(` ${relPath}: ${oldCall}${newCall}`);
totalFixes++;
}
}
// Fix backtick-quoted slash command references in prose
// Pattern: `/ command:name` references that point to non-existent commands
// These are documentation references - update to point to skill names
const proseRefFixes = {
'`/workflow:plan`': '`workflow-plan` skill',
'`/workflow:execute`': '`workflow-execute` skill',
'`/workflow:lite-execute`': '`workflow-lite-plan` skill',
'`/workflow:lite-fix`': '`workflow-lite-plan` skill',
'`/workflow:plan-verify`': '`workflow-plan` skill (plan-verify phase)',
'`/workflow:replan`': '`workflow-plan` skill (replan phase)',
'`/workflow:tdd-plan`': '`workflow-tdd` skill',
'`/workflow:tdd-verify`': '`workflow-tdd` skill (tdd-verify phase)',
'`/workflow:test-fix-gen`': '`workflow-test-fix` skill',
'`/workflow:test-gen`': '`workflow-test-fix` skill',
'`/workflow:test-cycle-execute`': '`workflow-test-fix` skill',
'`/workflow:review`': '`review-cycle` skill',
'`/workflow:review-session-cycle`': '`review-cycle` skill',
'`/workflow:review-module-cycle`': '`review-cycle` skill',
'`/workflow:review-cycle-fix`': '`review-cycle` skill (fix phase)',
'`/workflow:status`': '`workflow-execute` skill',
'`/workflow:brainstorm:artifacts`': '`brainstorm` skill',
'`/workflow:brainstorm:synthesis`': '`brainstorm` skill',
'`/workflow:brainstorm:role-analysis`': '`brainstorm` skill',
'`/memory:compact`': '`memory-capture` skill',
'`/memory:docs`': '`memory-manage` skill',
'`/compact`': '`memory-capture` skill',
'`/workflow:tools:context-gather`': '`workflow-plan` skill (context-gather phase)',
'`/workflow:tools:concept-enhanced`': '`workflow-test-fix` skill (concept-enhanced phase)',
'`/workflow:tools:task-generate`': '`workflow-plan` skill (task-generate phase)',
'`/workflow:ui-design:auto`': '`/workflow:ui-design:explore-auto`',
'`/workflow:ui-design:update`': '`/workflow:ui-design:generate`',
'`/workflow:multi-cli-plan`': '`workflow-multi-cli-plan` skill',
'`/workflow:lite-plan`': '`workflow-lite-plan` skill',
'`/cli:plan`': '`workflow-lite-plan` skill',
'`/test-cycle-execute`': '`workflow-test-fix` skill',
};
for (const [oldRef, newRef] of Object.entries(proseRefFixes)) {
if (content.includes(oldRef)) {
content = content.replaceAll(oldRef, newRef);
modified = true;
fixLog.push(` ${relPath}: ${oldRef}${newRef}`);
totalFixes++;
}
}
if (modified && !DRY_RUN) {
fs.writeFileSync(filePath, content);
}
}
for (const entry of fixLog) {
log(entry);
}
return totalFixes;
}
// ─── Main ───
console.log('=== CCW Ghost Command Cleanup ===');
console.log(`Root: ${ROOT}`);
console.log(`Mode: ${DRY_RUN ? 'DRY-RUN (no files modified)' : 'LIVE'}\n`);
// Step 1
console.log('Step 1: Scanning existing command files...');
const existingSources = getExistingCommandSources();
console.log(` Found ${existingSources.size} existing command files\n`);
// Step 2
console.log('Step 2: Cleaning JSON index files...');
let totalJsonRemoved = 0;
totalJsonRemoved += cleanAllCommandsJson(existingSources);
totalJsonRemoved += cleanEssentialCommandsJson(existingSources);
totalJsonRemoved += cleanByCategoryJson(existingSources);
totalJsonRemoved += cleanByUseCaseJson(existingSources);
totalJsonRemoved += cleanCommandRelationshipsJson(existingSources);
console.log(`\n Total JSON ghost entries removed: ${totalJsonRemoved}\n`);
// Step 3
console.log('Step 3: Fixing broken references in .md files...');
const totalMdFixes = fixBrokenReferences();
console.log(`\n Total .md reference fixes: ${totalMdFixes}\n`);
console.log('=== Done ===');
console.log(`Summary: ${totalJsonRemoved} JSON ghost entries, ${totalMdFixes} .md reference fixes`);
if (DRY_RUN) console.log('(No files were modified - run without --dry-run to apply changes)');