pixelcrew 0.2.2 → 0.2.6

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 (173) hide show
  1. package/README.md +389 -315
  2. package/bin/pixel-agents.js +306 -149
  3. package/package.json +3 -1
  4. package/skill/SKILL.md +185 -0
  5. package/skill/references/anti-ai-rubric.md +30 -0
  6. package/skill/references/anti-ai-slop-encyclopedia.md +181 -0
  7. package/skill/references/commands-reference.md +135 -0
  8. package/skill/references/testing-and-qa-strategy.md +25 -0
  9. package/skill/references/token-efficiency.md +20 -0
  10. package/src/adapters/adapter.interface.js +77 -0
  11. package/src/adapters/antigravity.js +138 -0
  12. package/src/adapters/claude-code.js +149 -0
  13. package/src/adapters/codex.js +131 -0
  14. package/src/adapters/cursor.js +133 -0
  15. package/src/adapters/generic.js +63 -0
  16. package/src/adapters/index.js +15 -0
  17. package/src/adapters/kiro.js +134 -0
  18. package/src/adapters/registry.js +150 -0
  19. package/src/backend/analyzer/complexity-analyzer.js +104 -0
  20. package/src/backend/analyzer/requirement-analyzer.js +213 -0
  21. package/src/backend/analyzer/threat-analyzer.js +122 -0
  22. package/src/backend/api/api-designer.js +90 -0
  23. package/src/backend/api/error-contract.js +132 -0
  24. package/src/backend/api/route-generator.js +92 -0
  25. package/src/backend/api/schema-generator.js +64 -0
  26. package/src/backend/architecture/adr-generator.js +62 -0
  27. package/src/backend/architecture/architecture-reasoner.js +119 -0
  28. package/src/backend/architecture/architecture-selector.js +57 -0
  29. package/src/backend/architecture/tradeoff-analyzer.js +92 -0
  30. package/src/backend/data/data-modeler.js +114 -0
  31. package/src/backend/data/index-analyzer.js +93 -0
  32. package/src/backend/data/query-analyzer.js +41 -0
  33. package/src/backend/data/schema-generator.js +92 -0
  34. package/src/backend/data/transaction-analyzer.js +49 -0
  35. package/src/backend/index.js +67 -0
  36. package/src/backend/observability/logging-engine.js +80 -0
  37. package/src/backend/observability/metrics-engine.js +31 -0
  38. package/src/backend/observability/tracing-engine.js +16 -0
  39. package/src/backend/patterns/pattern-registry.js +74 -0
  40. package/src/backend/patterns/pattern-selector.js +39 -0
  41. package/src/backend/patterns/tradeoff-engine.js +17 -0
  42. package/src/backend/performance/cache-analyzer.js +31 -0
  43. package/src/backend/performance/concurrency-analyzer.js +34 -0
  44. package/src/backend/performance/performance-engine.js +98 -0
  45. package/src/backend/performance/query-optimizer.js +16 -0
  46. package/src/backend/reliability/failure-analyzer.js +22 -0
  47. package/src/backend/reliability/idempotency-engine.js +56 -0
  48. package/src/backend/reliability/resilience-engine.js +141 -0
  49. package/src/backend/reliability/retry-analyzer.js +15 -0
  50. package/src/backend/security/auth-selector.js +33 -0
  51. package/src/backend/security/authorization-engine.js +90 -0
  52. package/src/backend/security/policy-engine.js +49 -0
  53. package/src/backend/security/secret-analyzer.js +39 -0
  54. package/src/backend/security/security-engine.js +122 -0
  55. package/src/backend/security/threat-model.js +59 -0
  56. package/src/backend/synthesis/backend-synthesizer.js +98 -0
  57. package/src/backend/synthesis/infrastructure-synthesizer.js +108 -0
  58. package/src/backend/synthesis/module-synthesizer.js +262 -0
  59. package/src/backend/validation/architecture-validator.js +70 -0
  60. package/src/backend/validation/backend-quality-validator.js +71 -0
  61. package/src/backend/validation/dependency-validator.js +20 -0
  62. package/src/backend/validation/performance-validator.js +38 -0
  63. package/src/backend/validation/security-validator.js +73 -0
  64. package/src/commands/adapt.js +105 -0
  65. package/src/commands/add.js +78 -0
  66. package/src/commands/animate.js +48 -0
  67. package/src/commands/assemble.js +57 -0
  68. package/src/commands/audit.js +93 -0
  69. package/src/commands/backend.js +125 -0
  70. package/src/commands/bento.js +25 -0
  71. package/src/commands/blueprint.js +65 -0
  72. package/src/commands/bolder.js +48 -0
  73. package/src/commands/boss-fight.js +60 -0
  74. package/src/commands/build.js +39 -0
  75. package/src/commands/calibrate.js +25 -0
  76. package/src/commands/chromatic.js +25 -0
  77. package/src/commands/clarify.js +52 -0
  78. package/src/commands/colorize.js +57 -0
  79. package/src/commands/command.interface.js +28 -0
  80. package/src/commands/commands.js +84 -0
  81. package/src/commands/craft.js +57 -0
  82. package/src/commands/crew.js +66 -0
  83. package/src/commands/critique.js +25 -0
  84. package/src/commands/de-slop.js +25 -0
  85. package/src/commands/delight.js +46 -0
  86. package/src/commands/deploy.js +42 -0
  87. package/src/commands/distill.js +48 -0
  88. package/src/commands/doctor.js +89 -0
  89. package/src/commands/document.js +83 -0
  90. package/src/commands/eight-bit.js +25 -0
  91. package/src/commands/extract.js +75 -0
  92. package/src/commands/fix.js +46 -0
  93. package/src/commands/harden.js +85 -0
  94. package/src/commands/index.js +67 -0
  95. package/src/commands/install.js +75 -0
  96. package/src/commands/layout.js +48 -0
  97. package/src/commands/live.js +44 -0
  98. package/src/commands/manifest.js +25 -0
  99. package/src/commands/office.js +25 -0
  100. package/src/commands/onboard.js +46 -0
  101. package/src/commands/oneshot.js +60 -0
  102. package/src/commands/optimize.js +46 -0
  103. package/src/commands/overdrive.js +46 -0
  104. package/src/commands/parser.js +143 -0
  105. package/src/commands/pixelcrew.js +96 -0
  106. package/src/commands/plan.js +66 -0
  107. package/src/commands/polish.js +45 -0
  108. package/src/commands/quieter.js +48 -0
  109. package/src/commands/recap.js +358 -0
  110. package/src/commands/registry.js +353 -0
  111. package/src/commands/render.js +26 -0
  112. package/src/commands/retrofit.js +25 -0
  113. package/src/commands/review.js +118 -0
  114. package/src/commands/roster.js +25 -0
  115. package/src/commands/sentinel.js +25 -0
  116. package/src/commands/shape.js +66 -0
  117. package/src/commands/skills.js +57 -0
  118. package/src/commands/status.js +48 -0
  119. package/src/commands/stop.js +50 -0
  120. package/src/commands/sync.js +58 -0
  121. package/src/commands/typeset.js +58 -0
  122. package/src/commands/warp.js +25 -0
  123. package/src/core/agent-runtime.js +94 -0
  124. package/src/core/event-bus.js +98 -0
  125. package/src/core/index.js +9 -0
  126. package/src/core/orchestrator.js +234 -0
  127. package/src/core/scheduler.js +124 -0
  128. package/src/core/task-graph.js +185 -0
  129. package/src/dashboard/app.js +1172 -30
  130. package/src/dashboard/index.html +223 -23
  131. package/src/dashboard/styles.css +870 -0
  132. package/src/index.js +16 -0
  133. package/src/orchestrator/artifact-graph.js +104 -0
  134. package/src/orchestrator/ast-normalizer.js +147 -0
  135. package/src/orchestrator/code-generator.js +846 -0
  136. package/src/orchestrator/design-engine.js +203 -0
  137. package/src/orchestrator/engine.js +318 -39
  138. package/src/orchestrator/oneshot.js +351 -1561
  139. package/src/orchestrator/ontology.js +394 -0
  140. package/src/orchestrator/planner.js +302 -0
  141. package/src/orchestrator/repair-planner.js +67 -0
  142. package/src/orchestrator/requirement-contract.js +177 -0
  143. package/src/orchestrator/requirement-validator.js +39 -0
  144. package/src/orchestrator/semantic-engine.js +582 -0
  145. package/src/orchestrator/semantic-reviewer.js +179 -0
  146. package/src/orchestrator/skills-registry.js +194 -0
  147. package/src/orchestrator/task-planner.js +111 -0
  148. package/src/orchestrator/task-queue.js +128 -0
  149. package/src/orchestrator/telemetry.js +209 -0
  150. package/src/orchestrator/ui-synthesizer.js +245 -0
  151. package/src/orchestrator/visual-critic.js +106 -0
  152. package/src/policies/architecture/layering.policy.js +14 -0
  153. package/src/policies/performance/indexing.policy.js +13 -0
  154. package/src/policies/reliability/resilience.policy.js +13 -0
  155. package/src/policies/security/auth.policy.js +31 -0
  156. package/src/protocol/agent.js +124 -0
  157. package/src/protocol/event.js +78 -0
  158. package/src/protocol/index.js +8 -0
  159. package/src/protocol/skill.js +54 -0
  160. package/src/protocol/task.js +100 -0
  161. package/src/scaffold/claude-generator.js +60 -0
  162. package/src/scaffold/commands-catalog.js +321 -0
  163. package/src/scaffold/cursor-generator.js +74 -0
  164. package/src/scaffold/ide-rules.js +133 -0
  165. package/src/scaffold/init.js +415 -91
  166. package/src/scaffold/installer.js +571 -0
  167. package/src/scaffold/kiro-generator.js +86 -0
  168. package/src/scaffold/skills-bundle.js +520 -0
  169. package/src/scaffold/templates.js +57 -4
  170. package/src/scaffold/workstations.js +364 -0
  171. package/src/server/server.js +292 -42
  172. package/src/utils/fs-safe.js +168 -0
  173. package/src/utils/markdown-report.js +175 -0
@@ -4,10 +4,16 @@ import fsSync from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
 
7
- export function createServer(engine, options = {}) {
8
- const rootDir = engine.rootDir;
9
- const dashboardDir = path.join(rootDir, '.pixel-agents', 'dashboard');
10
- const legacyDashboardDir = path.join(rootDir, '.pixel-dashboard');
7
+ export function createServer(engineOrRootDir, maybeEngine, options = {}) {
8
+ let engine = engineOrRootDir;
9
+ let rootDir = engine?.rootDir || process.cwd();
10
+
11
+ if (typeof engineOrRootDir === 'string') {
12
+ rootDir = engineOrRootDir;
13
+ engine = maybeEngine;
14
+ }
15
+
16
+ const dashboardDir = path.join(rootDir, '.pixel-crew', 'dashboard');
11
17
  const fallbackDashboardDir = fileURLToPath(new URL('../dashboard', import.meta.url));
12
18
 
13
19
  const sseClients = new Set();
@@ -80,15 +86,39 @@ export function createServer(engine, options = {}) {
80
86
 
81
87
  // API Routes
82
88
  if (pathname === '/api/info' && req.method === 'GET') {
89
+ let activeProv = { activeProvider: 'generic', activeProviderName: 'Generic CLI Runner', activeProviderIcon: '💻' };
90
+ if (engine.providerRegistry) {
91
+ try {
92
+ const scan = await engine.providerRegistry.scanEnvironment();
93
+ activeProv = {
94
+ activeProvider: scan.activeProvider,
95
+ activeProviderName: scan.activeProviderName,
96
+ activeProviderIcon: scan.activeProviderIcon
97
+ };
98
+ } catch {}
99
+ }
83
100
  return sendJson(200, {
84
101
  rootDir: engine.rootDir,
85
102
  project: engine.getConfig()?.project || path.basename(engine.rootDir),
86
- version: '0.1.0'
103
+ version: '0.2.5',
104
+ ...activeProv
87
105
  });
88
106
  }
89
107
 
90
108
  if (pathname === '/api/state' && req.method === 'GET') {
91
- return sendJson(200, engine.getState());
109
+ const state = engine.getState() || {};
110
+ let activeProv = { activeProvider: 'generic', activeProviderName: 'Generic CLI Runner', activeProviderIcon: '💻' };
111
+ if (engine.providerRegistry) {
112
+ try {
113
+ const scan = await engine.providerRegistry.scanEnvironment();
114
+ activeProv = {
115
+ activeProvider: scan.activeProvider,
116
+ activeProviderName: scan.activeProviderName,
117
+ activeProviderIcon: scan.activeProviderIcon
118
+ };
119
+ } catch {}
120
+ }
121
+ return sendJson(200, { ...state, ...activeProv });
92
122
  }
93
123
 
94
124
  if (pathname === '/api/config' && req.method === 'GET') {
@@ -103,11 +133,24 @@ export function createServer(engine, options = {}) {
103
133
  'Connection': 'keep-alive'
104
134
  });
105
135
 
136
+ let activeProv = { activeProvider: 'generic', activeProviderName: 'Generic CLI Runner', activeProviderIcon: '💻' };
137
+ if (engine.providerRegistry) {
138
+ try {
139
+ const scan = await engine.providerRegistry.scanEnvironment();
140
+ activeProv = {
141
+ activeProvider: scan.activeProvider,
142
+ activeProviderName: scan.activeProviderName,
143
+ activeProviderIcon: scan.activeProviderIcon
144
+ };
145
+ } catch {}
146
+ }
147
+
106
148
  // Send initial history
107
149
  const initialPayload = `event: init\ndata: ${JSON.stringify({
108
- state: engine.getState(),
150
+ state: { ...(engine.getState() || {}), ...activeProv },
109
151
  config: engine.getConfig(),
110
- history: engine.getEvents()
152
+ history: engine.getEvents(),
153
+ provider: activeProv
111
154
  })}\n\n`;
112
155
  res.write(initialPayload);
113
156
 
@@ -119,6 +162,82 @@ export function createServer(engine, options = {}) {
119
162
  return;
120
163
  }
121
164
 
165
+ if (pathname === '/api/command' && req.method === 'POST') {
166
+ const body = await readBody();
167
+ const input = body.input || body.command || body.prompt || '';
168
+ try {
169
+ const result = await engine.executeCommand(input, body.options || {});
170
+ return sendJson(200, result);
171
+ } catch (err) {
172
+ return sendJson(500, { success: false, error: err.message });
173
+ }
174
+ }
175
+
176
+ if (pathname === '/api/commands/autocomplete' && req.method === 'GET') {
177
+ const query = url.searchParams.get('q') || '';
178
+ const suggestions = engine.commandRegistry ? engine.commandRegistry.getAutocompleteSuggestions(query) : [];
179
+ return sendJson(200, { suggestions });
180
+ }
181
+
182
+ if (pathname === '/api/providers' && req.method === 'GET') {
183
+ if (engine.providerRegistry) {
184
+ const scan = await engine.providerRegistry.scanEnvironment(true);
185
+ return sendJson(200, {
186
+ activeProvider: scan.activeProvider,
187
+ activeProviderName: scan.activeProviderName,
188
+ activeProviderIcon: scan.activeProviderIcon,
189
+ activeProviderDescription: scan.activeProviderDescription,
190
+ available: scan.available.map(a => ({
191
+ id: a.id,
192
+ name: a.name,
193
+ icon: a.icon || '🤖',
194
+ description: a.description,
195
+ capabilities: a.capabilities
196
+ })),
197
+ missing: scan.missing.map(a => ({
198
+ id: a.id,
199
+ name: a.name,
200
+ icon: a.icon || '🤖'
201
+ }))
202
+ });
203
+ }
204
+ return sendJson(200, {
205
+ activeProvider: 'generic',
206
+ activeProviderName: 'Generic CLI Runner',
207
+ activeProviderIcon: '💻',
208
+ available: [{ id: 'generic', name: 'Generic CLI Runner', icon: '💻' }],
209
+ missing: []
210
+ });
211
+ }
212
+
213
+ if (pathname === '/api/providers/select' && req.method === 'POST') {
214
+ const body = await readBody();
215
+ const providerId = body.provider || body.id;
216
+ if (engine.providerRegistry && providerId) {
217
+ const adapter = engine.providerRegistry.getAdapter(providerId);
218
+ if (adapter) {
219
+ return sendJson(200, {
220
+ success: true,
221
+ activeProvider: adapter.id,
222
+ activeProviderName: adapter.name,
223
+ activeProviderIcon: adapter.icon || '🤖'
224
+ });
225
+ }
226
+ }
227
+ return sendJson(400, { success: false, error: 'Provider not found' });
228
+ }
229
+
230
+ if (pathname === '/api/plan' && req.method === 'POST') {
231
+ const body = await readBody();
232
+ const prompt = body.prompt || 'Synthesize responsive web architecture';
233
+ try {
234
+ const result = await engine.executeCommand(`/plan ${prompt}`, body.options || {});
235
+ return sendJson(200, result);
236
+ } catch (err) {
237
+ return sendJson(500, { success: false, error: err.message });
238
+ }
239
+ }
240
+
122
241
  if (pathname === '/api/task' && req.method === 'POST') {
123
242
  const body = await readBody();
124
243
  const prompt = body.prompt || body.task || 'Optimize application workflow';
@@ -127,25 +246,39 @@ export function createServer(engine, options = {}) {
127
246
  return sendJson(200, { status: 'accepted', prompt });
128
247
  }
129
248
 
130
- if (pathname === '/api/oneshot' && req.method === 'POST') {
249
+ if ((pathname === '/api/goal' || pathname === '/api/oneshot') && req.method === 'POST') {
131
250
  const body = await readBody();
132
- const prompt = body.prompt || 'Build a modern portfolio for an AI engineer inspired by editorial design';
251
+ const prompt = body.prompt || body.goal || 'Build a modern website for a design agency specializing in AI products';
133
252
  const options = {
134
- targetFramework: body.targetFramework || 'vanilla',
135
- outputDir: body.outputDir || path.join(engine.rootDir, 'generated-site')
253
+ targetFramework: body.targetFramework || 'nextjs',
254
+ outputDir: body.outputDir
136
255
  };
137
- // Asynchronously start OneShot pipeline
256
+ // Asynchronously start Multi-Agent Synthesis / Goal pipeline
138
257
  engine.submitOneShotTask(prompt, options).catch(console.error);
139
- return sendJson(200, { status: 'oneshot_started', prompt, options });
258
+ return sendJson(200, { status: 'goal_started', prompt, options });
140
259
  }
141
260
 
142
- if (pathname === '/api/token-stats' && req.method === 'GET') {
143
- return sendJson(200, {
261
+ if ((pathname === '/api/token-stats' || pathname === '/api/token-telemetry') && req.method === 'GET') {
262
+ const stats = engine.getTokenTelemetry ? engine.getTokenTelemetry() : null;
263
+ return sendJson(200, stats || {
144
264
  rawTokensEstimated: 42500,
145
265
  actualTokensUsed: 11800,
146
266
  tokensSaved: 30700,
147
267
  efficiencyRatio: 72,
148
268
  savingsPercent: "72%",
269
+ costUsd: 0.0284,
270
+ perAgent: {
271
+ creativeDirector: 2400,
272
+ uxPlanner: 2140,
273
+ designSystem: 1850,
274
+ frontend: 6200,
275
+ backend: 2300,
276
+ database: 1770,
277
+ performance: 1530,
278
+ security: 1400,
279
+ qa: 2800,
280
+ orchestrator: 1200
281
+ },
149
282
  strategiesActive: [
150
283
  "AST Symbol Graph Skeletonization",
151
284
  "Tiered Sliding Window Context Pruning",
@@ -157,15 +290,16 @@ export function createServer(engine, options = {}) {
157
290
  }
158
291
 
159
292
  if (pathname === '/api/site-preview' && req.method === 'GET') {
160
- const sitePath = path.join(engine.rootDir, 'generated-site', 'index.html');
161
- try {
162
- const html = await fs.readFile(sitePath, 'utf-8');
163
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
164
- res.end(html);
165
- return;
166
- } catch {
167
- return sendJson(404, { error: 'No generated site found yet. Run oneshot first.' });
293
+ const sitePath = engine.lastGeneratedSitePath || (engine.lastGeneratedOutputDir ? path.join(engine.lastGeneratedOutputDir, 'index.html') : null);
294
+ if (sitePath) {
295
+ try {
296
+ const html = await fs.readFile(sitePath, 'utf-8');
297
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
298
+ res.end(html);
299
+ return;
300
+ } catch {}
168
301
  }
302
+ return sendJson(404, { error: 'No generated site preview found yet. Execute a task or goal first.' });
169
303
  }
170
304
 
171
305
  if (pathname === '/api/emit' && req.method === 'POST') {
@@ -268,24 +402,118 @@ export function createServer(engine, options = {}) {
268
402
  heartbeat.unref();
269
403
 
270
404
  let isWatching = false;
405
+ let workspaceWatcher = null;
406
+ const debouncedFiles = new Map();
271
407
 
272
- server.once('listening', () => {
408
+ function inferAgentFromFile(relPath) {
409
+ const p = relPath.toLowerCase();
410
+ if (p.includes('test') || p.includes('spec') || p.includes('playwright') || p.includes('vitest') || p.includes('cypress')) {
411
+ return { agent: 'qa', skill: 'playwright-e2e' };
412
+ }
413
+ if (p.includes('prisma') || p.includes('schema') || p.includes('sql') || p.includes('database') || p.includes('migration') || p.includes('postgres')) {
414
+ return { agent: 'database', skill: 'postgresql' };
415
+ }
416
+ if (p.includes('auth') || p.includes('security') || p.includes('sentinel') || p.includes('policy') || p.includes('cert')) {
417
+ return { agent: 'security', skill: 'security-audit' };
418
+ }
419
+ if (p.includes('perf') || p.includes('benchmark') || p.includes('docker') || p.includes('k8s') || p.includes('sre') || p.includes('lcp')) {
420
+ return { agent: 'performance', skill: 'performance-profiling' };
421
+ }
422
+ if (p.includes('api/') || p.includes('server/') || p.includes('routes/') || p.includes('services/') || p.includes('backend') || p.endsWith('.go') || p.endsWith('.py') || p.endsWith('.rs')) {
423
+ return { agent: 'backend', skill: 'backend' };
424
+ }
425
+ if (p.endsWith('.css') || p.endsWith('.scss') || p.includes('tailwind') || p.includes('theme') || p.includes('tokens')) {
426
+ return { agent: 'frontend', skill: 'design-tokens' };
427
+ }
428
+ return { agent: 'frontend', skill: 'react' };
429
+ }
430
+
431
+ function startWatchers() {
273
432
  if (isWatching) return;
274
433
  isWatching = true;
275
434
 
276
- // Watch events.jsonl for direct external CLI / IDE writes
277
- let lastKnownEventCount = 0;
278
- if (engine.eventsPath) {
435
+ // 1. Live Workspace File Watcher (Captures IDE code edits in real-time)
436
+ try {
437
+ workspaceWatcher = fsSync.watch(rootDir, { recursive: true }, (eventType, filename) => {
438
+ if (!filename) return;
439
+
440
+ // Filter out noisy / internal directories
441
+ const normalized = filename.replace(/\\/g, '/');
442
+ if (
443
+ normalized.startsWith('.git') ||
444
+ normalized.includes('node_modules') ||
445
+ normalized.includes('.next') ||
446
+ normalized.includes('.vite') ||
447
+ normalized.includes('dist') ||
448
+ normalized.includes('build') ||
449
+ normalized.includes('coverage') ||
450
+ normalized.includes('.DS_Store') ||
451
+ normalized.includes('events.jsonl') ||
452
+ normalized.includes('state.json') ||
453
+ normalized.includes('daemon.json') ||
454
+ normalized.startsWith('.gemini') ||
455
+ normalized.endsWith('.log')
456
+ ) {
457
+ return;
458
+ }
459
+
460
+ const now = Date.now();
461
+ const lastSeen = debouncedFiles.get(normalized) || 0;
462
+ if (now - lastSeen < 200) return; // Debounce rapid writes
463
+ debouncedFiles.set(normalized, now);
464
+
465
+ const inferred = inferAgentFromFile(normalized);
466
+ const event = {
467
+ agent: inferred.agent,
468
+ type: 'tool',
469
+ skill: inferred.skill,
470
+ message: `Edited ${normalized} (+live IDE workspace update)`,
471
+ timestamp: now,
472
+ metadata: {
473
+ file: normalized,
474
+ action: 'EDIT',
475
+ source: 'ide_realtime_watcher'
476
+ }
477
+ };
478
+
479
+ if (engine && typeof engine.emitEvent === 'function') {
480
+ engine.emitEvent(event).catch(() => {});
481
+ } else {
482
+ engine.eventHistory.push(event);
483
+ if (engine.eventHistory.length > 300) engine.eventHistory.shift();
484
+ const payload = `event: agent_event\ndata: ${JSON.stringify(event)}\n\n`;
485
+ for (const client of sseClients) {
486
+ try { client.write(payload); } catch {}
487
+ }
488
+ }
489
+ });
490
+
491
+ if (workspaceWatcher && typeof workspaceWatcher.on === 'function') {
492
+ workspaceWatcher.on('error', () => {});
493
+ }
494
+ } catch {}
495
+
496
+ // 2. Watch events.jsonl for direct external CLI / IDE writes
497
+ const possibleEventsPaths = [
498
+ engine?.eventsPath,
499
+ path.join(rootDir, '.pixel-crew', 'events.jsonl'),
500
+ path.join(rootDir, '.pixel-agents', 'events.jsonl')
501
+ ].filter(Boolean);
502
+
503
+ const uniqueEventsPaths = Array.from(new Set(possibleEventsPaths));
504
+
505
+ for (const ePath of uniqueEventsPaths) {
506
+ let lastKnownEventCount = 0;
279
507
  try {
280
- const initialContent = fsSync.readFileSync(engine.eventsPath, 'utf-8');
508
+ const initialContent = fsSync.readFileSync(ePath, 'utf-8');
281
509
  lastKnownEventCount = initialContent.trim().split('\n').filter(Boolean).length;
282
510
  } catch {
283
511
  lastKnownEventCount = 0;
284
512
  }
285
513
 
286
- fsSync.watchFile(engine.eventsPath, { interval: 150 }, async () => {
514
+ fsSync.watchFile(ePath, { interval: 150 }, async () => {
287
515
  try {
288
- const content = await fs.readFile(engine.eventsPath, 'utf-8');
516
+ const content = await fs.readFile(ePath, 'utf-8');
289
517
  const lines = content.trim().split('\n').filter(Boolean);
290
518
  if (lines.length > lastKnownEventCount) {
291
519
  const newLines = lines.slice(lastKnownEventCount);
@@ -294,7 +522,7 @@ export function createServer(engine, options = {}) {
294
522
  try {
295
523
  const event = JSON.parse(line);
296
524
  engine.eventHistory.push(event);
297
- if (engine.eventHistory.length > 200) engine.eventHistory.shift();
525
+ if (engine.eventHistory.length > 300) engine.eventHistory.shift();
298
526
 
299
527
  const payload = `event: agent_event\ndata: ${JSON.stringify(event)}\n\n`;
300
528
  for (const client of sseClients) {
@@ -307,13 +535,21 @@ export function createServer(engine, options = {}) {
307
535
  });
308
536
  }
309
537
 
310
- // Watch state.json for direct external updates
311
- if (engine.statePath) {
312
- fsSync.watchFile(engine.statePath, { interval: 150 }, async () => {
538
+ // 3. Watch state.json for direct external updates
539
+ const possibleStatePaths = [
540
+ engine?.statePath,
541
+ path.join(rootDir, '.pixel-crew', 'state.json'),
542
+ path.join(rootDir, '.pixel-agents', 'state.json')
543
+ ].filter(Boolean);
544
+
545
+ const uniqueStatePaths = Array.from(new Set(possibleStatePaths));
546
+
547
+ for (const sPath of uniqueStatePaths) {
548
+ fsSync.watchFile(sPath, { interval: 150 }, async () => {
313
549
  try {
314
- const content = await fs.readFile(engine.statePath, 'utf-8');
550
+ const content = await fs.readFile(sPath, 'utf-8');
315
551
  const state = JSON.parse(content);
316
- engine.state = state;
552
+ if (engine) engine.state = state;
317
553
  const payload = `event: state_change\ndata: ${JSON.stringify({ state })}\n\n`;
318
554
  for (const client of sseClients) {
319
555
  try { client.write(payload); } catch {}
@@ -321,17 +557,31 @@ export function createServer(engine, options = {}) {
321
557
  } catch {}
322
558
  });
323
559
  }
324
- });
560
+ }
561
+
562
+ // Start watchers when server begins listening
563
+ server.on('listening', startWatchers);
325
564
 
326
565
  server.on('close', () => {
327
566
  clearInterval(heartbeat);
328
- if (engine.eventsPath) {
329
- try { fsSync.unwatchFile(engine.eventsPath); } catch {}
567
+ isWatching = false;
568
+ if (workspaceWatcher) {
569
+ try { workspaceWatcher.close(); } catch {}
330
570
  }
331
- if (engine.statePath) {
332
- try { fsSync.unwatchFile(engine.statePath); } catch {}
571
+ const possiblePaths = [
572
+ engine?.eventsPath,
573
+ engine?.statePath,
574
+ path.join(rootDir, '.pixel-crew', 'events.jsonl'),
575
+ path.join(rootDir, '.pixel-agents', 'events.jsonl'),
576
+ path.join(rootDir, '.pixel-crew', 'state.json'),
577
+ path.join(rootDir, '.pixel-agents', 'state.json')
578
+ ].filter(Boolean);
579
+
580
+ for (const p of possiblePaths) {
581
+ try { fsSync.unwatchFile(p); } catch {}
333
582
  }
334
583
  });
335
584
 
336
585
  return server;
337
586
  }
587
+
@@ -0,0 +1,168 @@
1
+ /**
2
+ * PIXEL CREW — Safe File System Operations & Dry-Run Layer
3
+ *
4
+ * Provides atomic, reversible, and previewable filesystem write operations
5
+ * preventing unintentional mutations during development and testing.
6
+ */
7
+
8
+ import fs from 'node:fs/promises';
9
+ import fsSync from 'node:fs';
10
+ import path from 'node:path';
11
+
12
+ export class DryRunReporter {
13
+ constructor(targetDir = process.cwd()) {
14
+ this.targetDir = targetDir;
15
+ this.creates = new Set();
16
+ this.updates = new Set();
17
+ this.dirs = new Set();
18
+ }
19
+
20
+ recordCreate(filePath) {
21
+ const rel = path.relative(this.targetDir, filePath) || filePath;
22
+ this.creates.add(rel);
23
+ }
24
+
25
+ recordUpdate(filePath) {
26
+ const rel = path.relative(this.targetDir, filePath) || filePath;
27
+ this.updates.add(rel);
28
+ }
29
+
30
+ recordDir(dirPath) {
31
+ const rel = path.relative(this.targetDir, dirPath) || dirPath;
32
+ this.dirs.add(rel);
33
+ }
34
+
35
+ hasChanges() {
36
+ return this.creates.size > 0 || this.updates.size > 0 || this.dirs.size > 0;
37
+ }
38
+
39
+ formatOutput() {
40
+ const lines = [];
41
+ lines.push('\x1b[33m\x1b[1m[DRY RUN PREVIEW] — No files were written to disk.\x1b[0m\n');
42
+
43
+ if (!this.hasChanges()) {
44
+ lines.push(' No files or directories would be modified.');
45
+ return lines.join('\n');
46
+ }
47
+
48
+ if (this.creates.size > 0) {
49
+ lines.push('\x1b[32mWould create files:\x1b[0m');
50
+ for (const f of Array.from(this.creates).sort()) {
51
+ lines.push(` \x1b[90m+\x1b[0m \x1b[36m${f}\x1b[0m`);
52
+ }
53
+ lines.push('');
54
+ }
55
+
56
+ if (this.updates.size > 0) {
57
+ lines.push('\x1b[33mWould update files:\x1b[0m');
58
+ for (const f of Array.from(this.updates).sort()) {
59
+ lines.push(` \x1b[90m~\x1b[0m \x1b[33m${f}\x1b[0m`);
60
+ }
61
+ lines.push('');
62
+ }
63
+
64
+ if (this.dirs.size > 0) {
65
+ lines.push('\x1b[35mWould ensure directories:\x1b[0m');
66
+ for (const d of Array.from(this.dirs).sort()) {
67
+ lines.push(` \x1b[90m📁\x1b[0m \x1b[35m${d}/\x1b[0m`);
68
+ }
69
+ lines.push('');
70
+ }
71
+
72
+ lines.push('\x1b[90mRun without \x1b[0m\x1b[33m--dry-run\x1b[0m\x1b[90m to apply these changes.\x1b[0m');
73
+ return lines.join('\n');
74
+ }
75
+
76
+ print() {
77
+ console.log('\n' + this.formatOutput() + '\n');
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Safely writes a file, supporting dry-run mode and directory auto-creation
83
+ */
84
+ export async function safeWriteFile(filePath, content, options = {}) {
85
+ const {
86
+ dryRun = false,
87
+ encoding = 'utf-8',
88
+ reporter = null,
89
+ targetDir = process.cwd()
90
+ } = options;
91
+
92
+ let exists = false;
93
+ try {
94
+ await fs.access(filePath);
95
+ exists = true;
96
+ } catch {
97
+ exists = false;
98
+ }
99
+
100
+ if (reporter) {
101
+ if (exists) {
102
+ reporter.recordUpdate(filePath);
103
+ } else {
104
+ reporter.recordCreate(filePath);
105
+ }
106
+ }
107
+
108
+ if (dryRun) {
109
+ return { written: false, dryRun: true, path: filePath, action: exists ? 'update' : 'create' };
110
+ }
111
+
112
+ // Ensure parent directory exists
113
+ const parentDir = path.dirname(filePath);
114
+ await fs.mkdir(parentDir, { recursive: true });
115
+
116
+ await fs.writeFile(filePath, content, encoding);
117
+ return { written: true, dryRun: false, path: filePath, action: exists ? 'update' : 'create' };
118
+ }
119
+
120
+ /**
121
+ * Safely creates a directory, supporting dry-run mode
122
+ */
123
+ export async function safeMkdir(dirPath, options = {}) {
124
+ const { dryRun = false, recursive = true, reporter = null } = options;
125
+
126
+ if (reporter) {
127
+ reporter.recordDir(dirPath);
128
+ }
129
+
130
+ if (dryRun) {
131
+ return { created: false, dryRun: true, path: dirPath };
132
+ }
133
+
134
+ await fs.mkdir(dirPath, { recursive });
135
+ return { created: true, dryRun: false, path: dirPath };
136
+ }
137
+
138
+ /**
139
+ * Safely copies a file, supporting dry-run mode
140
+ */
141
+ export async function safeCopyFile(srcPath, destPath, options = {}) {
142
+ const { dryRun = false, reporter = null } = options;
143
+
144
+ let exists = false;
145
+ try {
146
+ await fs.access(destPath);
147
+ exists = true;
148
+ } catch {
149
+ exists = false;
150
+ }
151
+
152
+ if (reporter) {
153
+ if (exists) {
154
+ reporter.recordUpdate(destPath);
155
+ } else {
156
+ reporter.recordCreate(destPath);
157
+ }
158
+ }
159
+
160
+ if (dryRun) {
161
+ return { copied: false, dryRun: true, path: destPath, action: exists ? 'update' : 'create' };
162
+ }
163
+
164
+ const parentDir = path.dirname(destPath);
165
+ await fs.mkdir(parentDir, { recursive: true });
166
+ await fs.copyFile(srcPath, destPath);
167
+ return { copied: true, dryRun: false, path: destPath, action: exists ? 'update' : 'create' };
168
+ }