ai-control-center 1.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +584 -0
- package/bin/aicc.js +772 -0
- package/lib/actions/approve.js +71 -0
- package/lib/actions/assign-project.js +132 -0
- package/lib/actions/browser-test.js +64 -0
- package/lib/actions/cleanup.js +174 -0
- package/lib/actions/debug.js +298 -0
- package/lib/actions/deploy.js +1229 -0
- package/lib/actions/fix-bug.js +134 -0
- package/lib/actions/new-feature.js +255 -0
- package/lib/actions/reject.js +307 -0
- package/lib/actions/review.js +706 -0
- package/lib/actions/status.js +47 -0
- package/lib/agents/browser-qa-agent.js +611 -0
- package/lib/agents/payment-agent.js +116 -0
- package/lib/agents/suggestion-agent.js +88 -0
- package/lib/cli.js +303 -0
- package/lib/config.js +243 -0
- package/lib/hub/hub-server.js +440 -0
- package/lib/hub/project-poller.js +75 -0
- package/lib/hub/skill-registry.js +89 -0
- package/lib/hub/state-aggregator.js +204 -0
- package/lib/index.js +471 -0
- package/lib/init/doctor.js +523 -0
- package/lib/init/presets.js +222 -0
- package/lib/init/skill-fetcher.js +77 -0
- package/lib/init/wizard.js +973 -0
- package/lib/integrations/codex-runner.js +128 -0
- package/lib/integrations/github-actions.js +248 -0
- package/lib/integrations/github-reporter.js +229 -0
- package/lib/integrations/screenshot-store.js +102 -0
- package/lib/openclaw/bridge.js +650 -0
- package/lib/openclaw/generate-skill.js +235 -0
- package/lib/openclaw/openclaw.json +64 -0
- package/lib/orchestrator/autonomous-loop.js +429 -0
- package/lib/orchestrator/thread-triggers.js +63 -0
- package/lib/roleplay/agent-messenger.js +75 -0
- package/lib/roleplay/discussion-threads.js +303 -0
- package/lib/roleplay/health-monitor.js +121 -0
- package/lib/roleplay/pm-agent.js +513 -0
- package/lib/roleplay/roleplay-config.js +25 -0
- package/lib/roleplay/room.js +164 -0
- package/lib/shared/action-runner.js +2330 -0
- package/lib/shared/event-bus.js +185 -0
- package/lib/slack/bot.js +378 -0
- package/lib/telegram/bot.js +416 -0
- package/lib/telegram/commands.js +1267 -0
- package/lib/telegram/keyboards.js +113 -0
- package/lib/telegram/notifications.js +247 -0
- package/lib/twitch/bot.js +354 -0
- package/lib/twitch/commands.js +302 -0
- package/lib/twitch/notifications.js +63 -0
- package/lib/utils/achievements.js +191 -0
- package/lib/utils/activity-log.js +182 -0
- package/lib/utils/agent-leaderboard.js +119 -0
- package/lib/utils/audit-logger.js +232 -0
- package/lib/utils/codebase-context.js +288 -0
- package/lib/utils/codebase-indexer.js +381 -0
- package/lib/utils/config-schema.js +230 -0
- package/lib/utils/context-compressor.js +172 -0
- package/lib/utils/correlation.js +63 -0
- package/lib/utils/cost-tracker.js +423 -0
- package/lib/utils/cron-scheduler.js +53 -0
- package/lib/utils/db-adapter.js +293 -0
- package/lib/utils/display.js +272 -0
- package/lib/utils/errors.js +116 -0
- package/lib/utils/format.js +134 -0
- package/lib/utils/intent-engine.js +464 -0
- package/lib/utils/mcp-client.js +238 -0
- package/lib/utils/model-ab-test.js +164 -0
- package/lib/utils/notify.js +122 -0
- package/lib/utils/persona-loader.js +80 -0
- package/lib/utils/pipeline-lock.js +73 -0
- package/lib/utils/pipeline.js +214 -0
- package/lib/utils/plugin-runner.js +234 -0
- package/lib/utils/rate-limiter.js +84 -0
- package/lib/utils/rbac.js +74 -0
- package/lib/utils/runner.js +1809 -0
- package/lib/utils/security.js +191 -0
- package/lib/utils/self-healer.js +144 -0
- package/lib/utils/skill-loader.js +255 -0
- package/lib/utils/spinner.js +132 -0
- package/lib/utils/stage-queue.js +50 -0
- package/lib/utils/state-machine.js +89 -0
- package/lib/utils/status-bar.js +327 -0
- package/lib/utils/token-estimator.js +101 -0
- package/lib/utils/ux-analyzer.js +101 -0
- package/lib/utils/webhook-emitter.js +83 -0
- package/lib/web/public/css/styles.css +417 -0
- package/lib/web/public/dark-mode.js +44 -0
- package/lib/web/public/hub/kanban.html +206 -0
- package/lib/web/public/index.html +45 -0
- package/lib/web/public/js/app.js +71 -0
- package/lib/web/public/js/ask.js +110 -0
- package/lib/web/public/js/dashboard.js +165 -0
- package/lib/web/public/js/deploy.js +72 -0
- package/lib/web/public/js/feature.js +79 -0
- package/lib/web/public/js/health.js +65 -0
- package/lib/web/public/js/logs.js +93 -0
- package/lib/web/public/js/review.js +123 -0
- package/lib/web/public/js/ws-client.js +82 -0
- package/lib/web/public/office/css/office.css +678 -0
- package/lib/web/public/office/index.html +148 -0
- package/lib/web/public/office/js/achievements-ui.js +117 -0
- package/lib/web/public/office/js/character.js +1056 -0
- package/lib/web/public/office/js/chat-bubbles.js +177 -0
- package/lib/web/public/office/js/cost-overlay.js +123 -0
- package/lib/web/public/office/js/day-night.js +68 -0
- package/lib/web/public/office/js/effects.js +632 -0
- package/lib/web/public/office/js/engine.js +146 -0
- package/lib/web/public/office/js/feature-ticket.js +216 -0
- package/lib/web/public/office/js/hub-client.js +60 -0
- package/lib/web/public/office/js/main.js +1757 -0
- package/lib/web/public/office/js/office-layout.js +1524 -0
- package/lib/web/public/office/js/pathfinding.js +144 -0
- package/lib/web/public/office/js/pixel-sprites.js +1454 -0
- package/lib/web/public/office/js/progress-bars.js +117 -0
- package/lib/web/public/office/js/replay.js +191 -0
- package/lib/web/public/office/js/sound-effects.js +91 -0
- package/lib/web/public/office/js/sprite-renderer.js +211 -0
- package/lib/web/public/office/js/stamina-system.js +89 -0
- package/lib/web/public/office/js/ui.js +107 -0
- package/lib/web/public/onboarding/index.html +243 -0
- package/lib/web/public/timeline/index.html +195 -0
- package/lib/web/routes/api.js +499 -0
- package/lib/web/routes/logs.js +20 -0
- package/lib/web/routes/metrics.js +99 -0
- package/lib/web/server.js +183 -0
- package/lib/web/ws/handler.js +65 -0
- package/package.json +67 -0
- package/templates/agent-architect.md +69 -0
- package/templates/agent-gemini-pm.md +49 -0
- package/templates/agent-gemini-reviewer.md +52 -0
- package/templates/copilot-instructions.md +36 -0
- package/templates/pipelines/mobile.json +27 -0
- package/templates/pipelines/nodejs-api.json +27 -0
- package/templates/pipelines/python.json +27 -0
- package/templates/pipelines/react.json +27 -0
- package/templates/pipelines/salesforce.json +27 -0
- package/templates/role-gemini.md +97 -0
- package/templates/skill-architect.md +114 -0
- package/templates/skill-browser-qa.md +50 -0
- package/templates/skill-bug-from-qa.md +58 -0
- package/templates/skill-chatbot.md +93 -0
- package/templates/skill-implement.md +78 -0
- package/templates/skill-openclaw.md +174 -0
- package/templates/skill-payment.md +110 -0
- package/templates/skill-pm-spec.md +77 -0
- package/templates/skill-requirement-capture.md +97 -0
- package/templates/skill-review.md +108 -0
- package/templates/skill-reviewer-qa.md +44 -0
- package/templates/skill-suggestion.md +45 -0
- package/templates/skill-template.md +142 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { printBox, printError, printInfo, printSuccess } from '../utils/display.js';
|
|
5
|
+
import { celebrate } from '../utils/notify.js';
|
|
6
|
+
import { getStatus, getWorkflowDir, updateStatus } from '../utils/pipeline.js';
|
|
7
|
+
import { cleanupAction } from './cleanup.js';
|
|
8
|
+
|
|
9
|
+
export async function approveAction() {
|
|
10
|
+
const status = getStatus();
|
|
11
|
+
|
|
12
|
+
if (status.stage !== 'review_complete') {
|
|
13
|
+
printError(`Cannot approve — current stage is "${status.stage}". Need review_complete.`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { confirmed } = await inquirer.prompt([{
|
|
18
|
+
type: 'confirm',
|
|
19
|
+
name: 'confirmed',
|
|
20
|
+
message: `Approve feature "${status.current_feature}"?`,
|
|
21
|
+
default: false,
|
|
22
|
+
}]);
|
|
23
|
+
|
|
24
|
+
if (!confirmed) {
|
|
25
|
+
printInfo('Approval cancelled.');
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Create approval record inline (no external .sh dependency)
|
|
30
|
+
const _workflowDir = getWorkflowDir();
|
|
31
|
+
const _approvedDir = resolve(_workflowDir, 'approved');
|
|
32
|
+
if (!existsSync(_approvedDir)) mkdirSync(_approvedDir, { recursive: true });
|
|
33
|
+
const _reviewsDir = resolve(_workflowDir, 'reviews');
|
|
34
|
+
if (existsSync(_reviewsDir)) {
|
|
35
|
+
const _reviews = readdirSync(_reviewsDir).filter(f => f.endsWith('.md')).sort().reverse();
|
|
36
|
+
if (_reviews.length) copyFileSync(resolve(_reviewsDir, _reviews[0]), resolve(_approvedDir, _reviews[0]));
|
|
37
|
+
}
|
|
38
|
+
const _ts = new Date();
|
|
39
|
+
const _pad = n => String(n).padStart(2, '0');
|
|
40
|
+
const _stamp = `${_ts.getFullYear()}${_pad(_ts.getMonth()+1)}${_pad(_ts.getDate())}-${_pad(_ts.getHours())}${_pad(_ts.getMinutes())}${_pad(_ts.getSeconds())}`;
|
|
41
|
+
writeFileSync(resolve(_approvedDir, `APPROVAL-${_stamp}.md`), [
|
|
42
|
+
`# Approval Record`,
|
|
43
|
+
`**Feature:** ${status.current_feature}`,
|
|
44
|
+
`**Review ID:** ${status.latest_review || 'none'}`,
|
|
45
|
+
`**Approved at:** ${_ts.toLocaleString()}`,
|
|
46
|
+
`**Notes:** Approved via Control Center`,
|
|
47
|
+
`**Status:** APPROVED ✅`,
|
|
48
|
+
``,
|
|
49
|
+
].join('\n'));
|
|
50
|
+
const _approveHistory = [...(status.history || []), { id: status.current_feature, stage: 'approved', time: _ts.toISOString() }];
|
|
51
|
+
updateStatus({ stage: 'approved', approved_at: _ts.toISOString(), next: 'deploy', history: _approveHistory });
|
|
52
|
+
|
|
53
|
+
printSuccess('Feature approved!');
|
|
54
|
+
await celebrate('approved');
|
|
55
|
+
|
|
56
|
+
// Auto-cleanup intermediate files silently
|
|
57
|
+
printInfo('Cleaning up intermediate files...');
|
|
58
|
+
await cleanupAction(true);
|
|
59
|
+
|
|
60
|
+
printBox('Next Steps — Deploy', [
|
|
61
|
+
'1. Run dry-run validation:',
|
|
62
|
+
' sf project deploy start --dry-run --test-level RunLocalTests',
|
|
63
|
+
'',
|
|
64
|
+
'2. Deploy to org:',
|
|
65
|
+
' sf project deploy start --test-level RunLocalTests',
|
|
66
|
+
'',
|
|
67
|
+
' OR use the Deploy option in this menu.',
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assign Project — Entry point for "give the AI IT Department a website to manage."
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* /assign https://example.com "Fix all bugs and add dark mode"
|
|
6
|
+
*
|
|
7
|
+
* This action:
|
|
8
|
+
* 1. Validates the URL is reachable
|
|
9
|
+
* 2. Creates a project record in .ai-workflow/projects/
|
|
10
|
+
* 3. Generates a project plan via AI
|
|
11
|
+
* 4. Updates pipeline status
|
|
12
|
+
* 5. The autonomous loop then takes over.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
16
|
+
import { resolve } from 'path';
|
|
17
|
+
import { getConfig } from '../config.js';
|
|
18
|
+
import { getWorkflowDir, getStatus, updateStatus, atomicWriteSync } from '../utils/pipeline.js';
|
|
19
|
+
import { logActivity } from '../utils/activity-log.js';
|
|
20
|
+
import { bus } from '../shared/event-bus.js';
|
|
21
|
+
import { randomUUID } from 'crypto';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validate that a URL is reachable.
|
|
25
|
+
*/
|
|
26
|
+
async function validateUrl(url) {
|
|
27
|
+
try {
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
30
|
+
const response = await fetch(url, {
|
|
31
|
+
method: 'HEAD',
|
|
32
|
+
signal: controller.signal,
|
|
33
|
+
headers: { 'User-Agent': 'AICC-Validator/1.0' },
|
|
34
|
+
});
|
|
35
|
+
clearTimeout(timeout);
|
|
36
|
+
return { reachable: true, status: response.status };
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return { reachable: false, error: err.message };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Main assign action.
|
|
44
|
+
*/
|
|
45
|
+
export async function assignProjectAction({ url, goal, credentials, config: overrideConfig } = {}) {
|
|
46
|
+
const config = overrideConfig || getConfig();
|
|
47
|
+
|
|
48
|
+
if (!url) throw new Error('URL is required. Usage: /assign <url> "<goal>"');
|
|
49
|
+
|
|
50
|
+
// Normalize URL
|
|
51
|
+
let targetUrl = url;
|
|
52
|
+
if (!targetUrl.startsWith('http')) targetUrl = `https://${targetUrl}`;
|
|
53
|
+
|
|
54
|
+
logActivity('ASSIGN', `Assigning project: ${targetUrl}`, 'info');
|
|
55
|
+
|
|
56
|
+
// 1. Validate URL
|
|
57
|
+
const validation = await validateUrl(targetUrl);
|
|
58
|
+
if (!validation.reachable) {
|
|
59
|
+
const msg = `URL not reachable: ${targetUrl} — ${validation.error}`;
|
|
60
|
+
logActivity('ASSIGN', msg, 'error');
|
|
61
|
+
throw new Error(msg);
|
|
62
|
+
}
|
|
63
|
+
logActivity('ASSIGN', `URL reachable (HTTP ${validation.status})`, 'success');
|
|
64
|
+
|
|
65
|
+
// 2. Create project record
|
|
66
|
+
const projectId = `proj-${Date.now()}-${randomUUID().slice(0, 6)}`;
|
|
67
|
+
const projectDir = resolve(getWorkflowDir(), 'projects', projectId);
|
|
68
|
+
if (!existsSync(projectDir)) mkdirSync(projectDir, { recursive: true });
|
|
69
|
+
|
|
70
|
+
const projectRecord = {
|
|
71
|
+
id: projectId,
|
|
72
|
+
url: targetUrl,
|
|
73
|
+
goal: goal || 'Audit website, fix all bugs, suggest improvements',
|
|
74
|
+
credentials: credentials || null,
|
|
75
|
+
assignedAt: new Date().toISOString(),
|
|
76
|
+
status: 'active',
|
|
77
|
+
qaRuns: 0,
|
|
78
|
+
bugFixCycles: 0,
|
|
79
|
+
featuresImplemented: 0,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
atomicWriteSync(
|
|
83
|
+
resolve(projectDir, 'project.json'),
|
|
84
|
+
JSON.stringify(projectRecord, null, 2)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// 3. Update global status
|
|
88
|
+
updateStatus({
|
|
89
|
+
stage: 'assign',
|
|
90
|
+
featureId: projectId,
|
|
91
|
+
assignedUrl: targetUrl,
|
|
92
|
+
assignedGoal: goal || projectRecord.goal,
|
|
93
|
+
pipelineStarted: new Date().toISOString(),
|
|
94
|
+
currentCycle: 0,
|
|
95
|
+
maxCycles: config.browserQA?.maxBugfixCycles || 3,
|
|
96
|
+
transitionedAt: new Date().toISOString(),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// 4. Update browserQA config with the assigned URL (runtime override)
|
|
100
|
+
if (config.browserQA) {
|
|
101
|
+
config.browserQA.targetUrl = targetUrl;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 5. Emit event
|
|
105
|
+
bus.emit('pipeline-event', {
|
|
106
|
+
event: 'project_assigned',
|
|
107
|
+
data: { projectId, url: targetUrl, goal: projectRecord.goal },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
logActivity('ASSIGN', `Project assigned: ${projectId}. Starting QA audit...`, 'success');
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
success: true,
|
|
114
|
+
projectId,
|
|
115
|
+
url: targetUrl,
|
|
116
|
+
goal: projectRecord.goal,
|
|
117
|
+
nextStage: 'browser-qa',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get current project info.
|
|
123
|
+
*/
|
|
124
|
+
export function getCurrentProject() {
|
|
125
|
+
const status = getStatus();
|
|
126
|
+
if (!status.featureId?.startsWith('proj-')) return null;
|
|
127
|
+
|
|
128
|
+
const projectFile = resolve(getWorkflowDir(), 'projects', status.featureId, 'project.json');
|
|
129
|
+
if (!existsSync(projectFile)) return null;
|
|
130
|
+
|
|
131
|
+
return JSON.parse(readFileSync(projectFile, 'utf8'));
|
|
132
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Test Action — Pipeline stage wrapper for Browser QA Agent.
|
|
3
|
+
*
|
|
4
|
+
* This is a pipeline stage action (like new-feature.js, deploy.js).
|
|
5
|
+
* Called by action-runner.js when stage === 'browser-qa'.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getConfig } from '../config.js';
|
|
9
|
+
import { logActivity } from '../utils/activity-log.js';
|
|
10
|
+
import { runBrowserQA } from '../agents/browser-qa-agent.js';
|
|
11
|
+
import { bus } from '../shared/event-bus.js';
|
|
12
|
+
|
|
13
|
+
export async function browserTestAction(options = {}) {
|
|
14
|
+
const config = options.config || getConfig();
|
|
15
|
+
|
|
16
|
+
if (!config.browserQA?.enabled) {
|
|
17
|
+
logActivity('QA', 'Browser QA disabled in config — skipping', 'warn');
|
|
18
|
+
return { success: true, skipped: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
logActivity('QA', 'Starting browser QA stage', 'info');
|
|
22
|
+
bus.emitEvent('stage_start', { stage: 'browser-qa', role: 'QA Tester' });
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const report = await runBrowserQA(config);
|
|
26
|
+
|
|
27
|
+
if (report.skipped) {
|
|
28
|
+
bus.emitEvent('stage_complete', { stage: 'browser-qa', skipped: true });
|
|
29
|
+
return { success: true, skipped: true };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Report bugs to GitHub if configured
|
|
33
|
+
if (config.github?.enabled && report.failed.length > 0) {
|
|
34
|
+
try {
|
|
35
|
+
const { reportBugsToGitHub } = await import('../integrations/github-reporter.js');
|
|
36
|
+
await reportBugsToGitHub(report, config);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
logActivity('GITHUB', `Failed to report to GitHub: ${err.message}`, 'warn');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hasFails = report.failed.length > 0;
|
|
43
|
+
|
|
44
|
+
bus.emitEvent(hasFails ? 'stage_error' : 'stage_complete', {
|
|
45
|
+
stage: 'browser-qa',
|
|
46
|
+
role: 'QA Tester',
|
|
47
|
+
report: report.summary,
|
|
48
|
+
verdict: hasFails ? 'FAILED' : 'PASSED',
|
|
49
|
+
failedRoutes: report.failed.map(f => f.url),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
success: !hasFails,
|
|
54
|
+
report,
|
|
55
|
+
needsBugfix: hasFails,
|
|
56
|
+
failCount: report.failed.length,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
} catch (err) {
|
|
60
|
+
logActivity('QA', `Browser QA failed: ${err.message}`, 'error');
|
|
61
|
+
bus.emitEvent('stage_error', { stage: 'browser-qa', error: err.message });
|
|
62
|
+
return { success: false, error: err.message };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { existsSync, readdirSync, rmSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { getWorkflowDir } from '../utils/pipeline.js';
|
|
6
|
+
import { printSuccess, printInfo, printDivider } from '../utils/display.js';
|
|
7
|
+
|
|
8
|
+
// ─── Cleanup policy ────────────────────────────────────────────────────────────
|
|
9
|
+
//
|
|
10
|
+
// KEEP — permanent design record (user can review "why did we do this?"):
|
|
11
|
+
// specs/ PM requirements & user stories
|
|
12
|
+
// architecture/ Design decisions & class diagrams
|
|
13
|
+
// tasks/ Implementation plan Copilot followed
|
|
14
|
+
// implementation-notes/ Supplementary notes
|
|
15
|
+
// reviews/ Final approved review (latest only)
|
|
16
|
+
// sessions/ Copilot conversation history per feature
|
|
17
|
+
// approved/ Approval records
|
|
18
|
+
//
|
|
19
|
+
// DELETE — working/intermediate files (no long-term value):
|
|
20
|
+
// inbox/ Raw feature request (content already captured in spec)
|
|
21
|
+
// rejected/ Old rejection records
|
|
22
|
+
// deploy-fixes/ Temporary error analysis (DEPLOYFIX-*.md, VALIDATE-*.md)
|
|
23
|
+
// reviews/ (older) All but the latest review
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Called automatically after approve, or manually from the menu.
|
|
27
|
+
*/
|
|
28
|
+
export async function cleanupAction(silent = false) {
|
|
29
|
+
const workflowDir = getWorkflowDir();
|
|
30
|
+
|
|
31
|
+
const preview = buildCleanupPreview(workflowDir);
|
|
32
|
+
|
|
33
|
+
if (!silent) {
|
|
34
|
+
if (preview.toDelete.length === 0 && preview.toKeep.length === 0) {
|
|
35
|
+
printInfo('Nothing to clean up — workspace is already tidy.');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log('');
|
|
40
|
+
console.log(chalk.bold(' Cleanup Preview'));
|
|
41
|
+
printDivider();
|
|
42
|
+
|
|
43
|
+
if (preview.toKeep.length) {
|
|
44
|
+
console.log(chalk.green.bold('\n KEEP (permanent design record):'));
|
|
45
|
+
preview.toKeep.forEach(f => console.log(chalk.green(` ✓ ${f}`)));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (preview.toDelete.length) {
|
|
49
|
+
console.log(chalk.red.bold('\n REMOVE (working / intermediate files):'));
|
|
50
|
+
preview.toDelete.forEach(f => console.log(chalk.red(` ✗ ${f}`)));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('');
|
|
54
|
+
|
|
55
|
+
const { confirmed } = await inquirer.prompt([{
|
|
56
|
+
type: 'confirm',
|
|
57
|
+
name: 'confirmed',
|
|
58
|
+
message: `Remove ${preview.toDelete.length} intermediate file(s)?`,
|
|
59
|
+
default: true,
|
|
60
|
+
}]);
|
|
61
|
+
|
|
62
|
+
if (!confirmed) {
|
|
63
|
+
printInfo('Cleanup cancelled.');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
runCleanup(workflowDir, preview);
|
|
69
|
+
printSuccess('Workspace cleaned. Design record preserved in specs/, architecture/, tasks/, sessions/, reviews/.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Build preview ─────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function buildCleanupPreview(workflowDir) {
|
|
75
|
+
const toKeep = [];
|
|
76
|
+
const toDelete = [];
|
|
77
|
+
|
|
78
|
+
// ── KEEP: specs, architecture, tasks, implementation-notes ───────────────────
|
|
79
|
+
for (const dir of ['specs', 'architecture', 'tasks', 'implementation-notes']) {
|
|
80
|
+
const d = resolve(workflowDir, dir);
|
|
81
|
+
if (existsSync(d)) {
|
|
82
|
+
readdirSync(d).filter(f => f.endsWith('.md')).forEach(f => {
|
|
83
|
+
toKeep.push(`${dir}/${f}`);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── KEEP: sessions (Copilot conversation history per feature) ────────────────
|
|
89
|
+
const sessionsDir = resolve(workflowDir, 'sessions');
|
|
90
|
+
if (existsSync(sessionsDir)) {
|
|
91
|
+
readdirSync(sessionsDir).filter(f => f.endsWith('.md')).forEach(f => {
|
|
92
|
+
toKeep.push(`sessions/${f} (Copilot session history)`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── KEEP: approved records ────────────────────────────────────────────────────
|
|
97
|
+
const approvedDir = resolve(workflowDir, 'approved');
|
|
98
|
+
if (existsSync(approvedDir)) {
|
|
99
|
+
readdirSync(approvedDir).filter(f => f.endsWith('.md')).forEach(f => {
|
|
100
|
+
toKeep.push(`approved/${f} (approval record)`);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── KEEP latest review, DELETE older ones ────────────────────────────────────
|
|
105
|
+
const reviewsDir = resolve(workflowDir, 'reviews');
|
|
106
|
+
if (existsSync(reviewsDir)) {
|
|
107
|
+
const reviews = readdirSync(reviewsDir).filter(f => f.endsWith('.md')).sort().reverse();
|
|
108
|
+
reviews.forEach((f, i) => {
|
|
109
|
+
if (i === 0) toKeep.push(`reviews/${f} (final approved review)`);
|
|
110
|
+
else toDelete.push(`reviews/${f}`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── DELETE: inbox (raw request — already captured in spec) ───────────────────
|
|
115
|
+
const inboxDir = resolve(workflowDir, 'inbox');
|
|
116
|
+
if (existsSync(inboxDir)) {
|
|
117
|
+
readdirSync(inboxDir).filter(f => f.endsWith('.md')).forEach(f => {
|
|
118
|
+
toDelete.push(`inbox/${f}`);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── DELETE: rejected (historical rejection records) ───────────────────────────
|
|
123
|
+
const rejectedDir = resolve(workflowDir, 'rejected');
|
|
124
|
+
if (existsSync(rejectedDir)) {
|
|
125
|
+
readdirSync(rejectedDir).filter(f => f.endsWith('.md')).forEach(f => {
|
|
126
|
+
toDelete.push(`rejected/${f}`);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── DELETE: deploy-fixes (temporary error analysis & validation files) ────────
|
|
131
|
+
const deployFixesDir = resolve(workflowDir, 'deploy-fixes');
|
|
132
|
+
if (existsSync(deployFixesDir)) {
|
|
133
|
+
readdirSync(deployFixesDir).filter(f => f.endsWith('.md')).forEach(f => {
|
|
134
|
+
toDelete.push(`deploy-fixes/${f}`);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { toKeep, toDelete };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Execute cleanup ───────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
function runCleanup(workflowDir, preview) {
|
|
144
|
+
// Reviews: keep only the latest
|
|
145
|
+
const reviewsDir = resolve(workflowDir, 'reviews');
|
|
146
|
+
if (existsSync(reviewsDir)) {
|
|
147
|
+
const reviews = readdirSync(reviewsDir).filter(f => f.endsWith('.md')).sort().reverse();
|
|
148
|
+
reviews.slice(1).forEach(f => rmSync(resolve(reviewsDir, f)));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Inbox: delete all
|
|
152
|
+
const inboxDir = resolve(workflowDir, 'inbox');
|
|
153
|
+
if (existsSync(inboxDir)) {
|
|
154
|
+
readdirSync(inboxDir).filter(f => f.endsWith('.md')).forEach(f => {
|
|
155
|
+
rmSync(resolve(inboxDir, f));
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Rejected: delete all
|
|
160
|
+
const rejectedDir = resolve(workflowDir, 'rejected');
|
|
161
|
+
if (existsSync(rejectedDir)) {
|
|
162
|
+
readdirSync(rejectedDir).filter(f => f.endsWith('.md')).forEach(f => {
|
|
163
|
+
rmSync(resolve(rejectedDir, f));
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Deploy-fixes: delete all
|
|
168
|
+
const deployFixesDir = resolve(workflowDir, 'deploy-fixes');
|
|
169
|
+
if (existsSync(deployFixesDir)) {
|
|
170
|
+
readdirSync(deployFixesDir).filter(f => f.endsWith('.md')).forEach(f => {
|
|
171
|
+
rmSync(resolve(deployFixesDir, f));
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|