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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +584 -0
  3. package/bin/aicc.js +772 -0
  4. package/lib/actions/approve.js +71 -0
  5. package/lib/actions/assign-project.js +132 -0
  6. package/lib/actions/browser-test.js +64 -0
  7. package/lib/actions/cleanup.js +174 -0
  8. package/lib/actions/debug.js +298 -0
  9. package/lib/actions/deploy.js +1229 -0
  10. package/lib/actions/fix-bug.js +134 -0
  11. package/lib/actions/new-feature.js +255 -0
  12. package/lib/actions/reject.js +307 -0
  13. package/lib/actions/review.js +706 -0
  14. package/lib/actions/status.js +47 -0
  15. package/lib/agents/browser-qa-agent.js +611 -0
  16. package/lib/agents/payment-agent.js +116 -0
  17. package/lib/agents/suggestion-agent.js +88 -0
  18. package/lib/cli.js +303 -0
  19. package/lib/config.js +243 -0
  20. package/lib/hub/hub-server.js +440 -0
  21. package/lib/hub/project-poller.js +75 -0
  22. package/lib/hub/skill-registry.js +89 -0
  23. package/lib/hub/state-aggregator.js +204 -0
  24. package/lib/index.js +471 -0
  25. package/lib/init/doctor.js +523 -0
  26. package/lib/init/presets.js +222 -0
  27. package/lib/init/skill-fetcher.js +77 -0
  28. package/lib/init/wizard.js +973 -0
  29. package/lib/integrations/codex-runner.js +128 -0
  30. package/lib/integrations/github-actions.js +248 -0
  31. package/lib/integrations/github-reporter.js +229 -0
  32. package/lib/integrations/screenshot-store.js +102 -0
  33. package/lib/openclaw/bridge.js +650 -0
  34. package/lib/openclaw/generate-skill.js +235 -0
  35. package/lib/openclaw/openclaw.json +64 -0
  36. package/lib/orchestrator/autonomous-loop.js +429 -0
  37. package/lib/orchestrator/thread-triggers.js +63 -0
  38. package/lib/roleplay/agent-messenger.js +75 -0
  39. package/lib/roleplay/discussion-threads.js +303 -0
  40. package/lib/roleplay/health-monitor.js +121 -0
  41. package/lib/roleplay/pm-agent.js +513 -0
  42. package/lib/roleplay/roleplay-config.js +25 -0
  43. package/lib/roleplay/room.js +164 -0
  44. package/lib/shared/action-runner.js +2330 -0
  45. package/lib/shared/event-bus.js +185 -0
  46. package/lib/slack/bot.js +378 -0
  47. package/lib/telegram/bot.js +416 -0
  48. package/lib/telegram/commands.js +1267 -0
  49. package/lib/telegram/keyboards.js +113 -0
  50. package/lib/telegram/notifications.js +247 -0
  51. package/lib/twitch/bot.js +354 -0
  52. package/lib/twitch/commands.js +302 -0
  53. package/lib/twitch/notifications.js +63 -0
  54. package/lib/utils/achievements.js +191 -0
  55. package/lib/utils/activity-log.js +182 -0
  56. package/lib/utils/agent-leaderboard.js +119 -0
  57. package/lib/utils/audit-logger.js +232 -0
  58. package/lib/utils/codebase-context.js +288 -0
  59. package/lib/utils/codebase-indexer.js +381 -0
  60. package/lib/utils/config-schema.js +230 -0
  61. package/lib/utils/context-compressor.js +172 -0
  62. package/lib/utils/correlation.js +63 -0
  63. package/lib/utils/cost-tracker.js +423 -0
  64. package/lib/utils/cron-scheduler.js +53 -0
  65. package/lib/utils/db-adapter.js +293 -0
  66. package/lib/utils/display.js +272 -0
  67. package/lib/utils/errors.js +116 -0
  68. package/lib/utils/format.js +134 -0
  69. package/lib/utils/intent-engine.js +464 -0
  70. package/lib/utils/mcp-client.js +238 -0
  71. package/lib/utils/model-ab-test.js +164 -0
  72. package/lib/utils/notify.js +122 -0
  73. package/lib/utils/persona-loader.js +80 -0
  74. package/lib/utils/pipeline-lock.js +73 -0
  75. package/lib/utils/pipeline.js +214 -0
  76. package/lib/utils/plugin-runner.js +234 -0
  77. package/lib/utils/rate-limiter.js +84 -0
  78. package/lib/utils/rbac.js +74 -0
  79. package/lib/utils/runner.js +1809 -0
  80. package/lib/utils/security.js +191 -0
  81. package/lib/utils/self-healer.js +144 -0
  82. package/lib/utils/skill-loader.js +255 -0
  83. package/lib/utils/spinner.js +132 -0
  84. package/lib/utils/stage-queue.js +50 -0
  85. package/lib/utils/state-machine.js +89 -0
  86. package/lib/utils/status-bar.js +327 -0
  87. package/lib/utils/token-estimator.js +101 -0
  88. package/lib/utils/ux-analyzer.js +101 -0
  89. package/lib/utils/webhook-emitter.js +83 -0
  90. package/lib/web/public/css/styles.css +417 -0
  91. package/lib/web/public/dark-mode.js +44 -0
  92. package/lib/web/public/hub/kanban.html +206 -0
  93. package/lib/web/public/index.html +45 -0
  94. package/lib/web/public/js/app.js +71 -0
  95. package/lib/web/public/js/ask.js +110 -0
  96. package/lib/web/public/js/dashboard.js +165 -0
  97. package/lib/web/public/js/deploy.js +72 -0
  98. package/lib/web/public/js/feature.js +79 -0
  99. package/lib/web/public/js/health.js +65 -0
  100. package/lib/web/public/js/logs.js +93 -0
  101. package/lib/web/public/js/review.js +123 -0
  102. package/lib/web/public/js/ws-client.js +82 -0
  103. package/lib/web/public/office/css/office.css +678 -0
  104. package/lib/web/public/office/index.html +148 -0
  105. package/lib/web/public/office/js/achievements-ui.js +117 -0
  106. package/lib/web/public/office/js/character.js +1056 -0
  107. package/lib/web/public/office/js/chat-bubbles.js +177 -0
  108. package/lib/web/public/office/js/cost-overlay.js +123 -0
  109. package/lib/web/public/office/js/day-night.js +68 -0
  110. package/lib/web/public/office/js/effects.js +632 -0
  111. package/lib/web/public/office/js/engine.js +146 -0
  112. package/lib/web/public/office/js/feature-ticket.js +216 -0
  113. package/lib/web/public/office/js/hub-client.js +60 -0
  114. package/lib/web/public/office/js/main.js +1757 -0
  115. package/lib/web/public/office/js/office-layout.js +1524 -0
  116. package/lib/web/public/office/js/pathfinding.js +144 -0
  117. package/lib/web/public/office/js/pixel-sprites.js +1454 -0
  118. package/lib/web/public/office/js/progress-bars.js +117 -0
  119. package/lib/web/public/office/js/replay.js +191 -0
  120. package/lib/web/public/office/js/sound-effects.js +91 -0
  121. package/lib/web/public/office/js/sprite-renderer.js +211 -0
  122. package/lib/web/public/office/js/stamina-system.js +89 -0
  123. package/lib/web/public/office/js/ui.js +107 -0
  124. package/lib/web/public/onboarding/index.html +243 -0
  125. package/lib/web/public/timeline/index.html +195 -0
  126. package/lib/web/routes/api.js +499 -0
  127. package/lib/web/routes/logs.js +20 -0
  128. package/lib/web/routes/metrics.js +99 -0
  129. package/lib/web/server.js +183 -0
  130. package/lib/web/ws/handler.js +65 -0
  131. package/package.json +67 -0
  132. package/templates/agent-architect.md +69 -0
  133. package/templates/agent-gemini-pm.md +49 -0
  134. package/templates/agent-gemini-reviewer.md +52 -0
  135. package/templates/copilot-instructions.md +36 -0
  136. package/templates/pipelines/mobile.json +27 -0
  137. package/templates/pipelines/nodejs-api.json +27 -0
  138. package/templates/pipelines/python.json +27 -0
  139. package/templates/pipelines/react.json +27 -0
  140. package/templates/pipelines/salesforce.json +27 -0
  141. package/templates/role-gemini.md +97 -0
  142. package/templates/skill-architect.md +114 -0
  143. package/templates/skill-browser-qa.md +50 -0
  144. package/templates/skill-bug-from-qa.md +58 -0
  145. package/templates/skill-chatbot.md +93 -0
  146. package/templates/skill-implement.md +78 -0
  147. package/templates/skill-openclaw.md +174 -0
  148. package/templates/skill-payment.md +110 -0
  149. package/templates/skill-pm-spec.md +77 -0
  150. package/templates/skill-requirement-capture.md +97 -0
  151. package/templates/skill-review.md +108 -0
  152. package/templates/skill-reviewer-qa.md +44 -0
  153. package/templates/skill-suggestion.md +45 -0
  154. 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
+ }