pixelcrew 0.2.3 → 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 (171) 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 +632 -780
  136. package/src/orchestrator/design-engine.js +203 -0
  137. package/src/orchestrator/engine.js +307 -38
  138. package/src/orchestrator/oneshot.js +367 -380
  139. package/src/orchestrator/ontology.js +394 -0
  140. package/src/orchestrator/planner.js +231 -258
  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/task-planner.js +111 -0
  147. package/src/orchestrator/telemetry.js +209 -0
  148. package/src/orchestrator/ui-synthesizer.js +245 -0
  149. package/src/orchestrator/visual-critic.js +106 -0
  150. package/src/policies/architecture/layering.policy.js +14 -0
  151. package/src/policies/performance/indexing.policy.js +13 -0
  152. package/src/policies/reliability/resilience.policy.js +13 -0
  153. package/src/policies/security/auth.policy.js +31 -0
  154. package/src/protocol/agent.js +124 -0
  155. package/src/protocol/event.js +78 -0
  156. package/src/protocol/index.js +8 -0
  157. package/src/protocol/skill.js +54 -0
  158. package/src/protocol/task.js +100 -0
  159. package/src/scaffold/claude-generator.js +60 -0
  160. package/src/scaffold/commands-catalog.js +321 -0
  161. package/src/scaffold/cursor-generator.js +74 -0
  162. package/src/scaffold/ide-rules.js +133 -0
  163. package/src/scaffold/init.js +415 -91
  164. package/src/scaffold/installer.js +571 -0
  165. package/src/scaffold/kiro-generator.js +86 -0
  166. package/src/scaffold/skills-bundle.js +520 -0
  167. package/src/scaffold/templates.js +57 -4
  168. package/src/scaffold/workstations.js +364 -0
  169. package/src/server/server.js +283 -40
  170. package/src/utils/fs-safe.js +168 -0
  171. package/src/utils/markdown-report.js +175 -0
@@ -13,8 +13,7 @@ export function createServer(engineOrRootDir, maybeEngine, options = {}) {
13
13
  engine = maybeEngine;
14
14
  }
15
15
 
16
- const dashboardDir = path.join(rootDir, '.pixel-agents', 'dashboard');
17
- const legacyDashboardDir = path.join(rootDir, '.pixel-dashboard');
16
+ const dashboardDir = path.join(rootDir, '.pixel-crew', 'dashboard');
18
17
  const fallbackDashboardDir = fileURLToPath(new URL('../dashboard', import.meta.url));
19
18
 
20
19
  const sseClients = new Set();
@@ -87,15 +86,39 @@ export function createServer(engineOrRootDir, maybeEngine, options = {}) {
87
86
 
88
87
  // API Routes
89
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
+ }
90
100
  return sendJson(200, {
91
101
  rootDir: engine.rootDir,
92
102
  project: engine.getConfig()?.project || path.basename(engine.rootDir),
93
- version: '0.1.0'
103
+ version: '0.2.5',
104
+ ...activeProv
94
105
  });
95
106
  }
96
107
 
97
108
  if (pathname === '/api/state' && req.method === 'GET') {
98
- 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 });
99
122
  }
100
123
 
101
124
  if (pathname === '/api/config' && req.method === 'GET') {
@@ -110,11 +133,24 @@ export function createServer(engineOrRootDir, maybeEngine, options = {}) {
110
133
  'Connection': 'keep-alive'
111
134
  });
112
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
+
113
148
  // Send initial history
114
149
  const initialPayload = `event: init\ndata: ${JSON.stringify({
115
- state: engine.getState(),
150
+ state: { ...(engine.getState() || {}), ...activeProv },
116
151
  config: engine.getConfig(),
117
- history: engine.getEvents()
152
+ history: engine.getEvents(),
153
+ provider: activeProv
118
154
  })}\n\n`;
119
155
  res.write(initialPayload);
120
156
 
@@ -126,6 +162,82 @@ export function createServer(engineOrRootDir, maybeEngine, options = {}) {
126
162
  return;
127
163
  }
128
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
+
129
241
  if (pathname === '/api/task' && req.method === 'POST') {
130
242
  const body = await readBody();
131
243
  const prompt = body.prompt || body.task || 'Optimize application workflow';
@@ -134,25 +246,39 @@ export function createServer(engineOrRootDir, maybeEngine, options = {}) {
134
246
  return sendJson(200, { status: 'accepted', prompt });
135
247
  }
136
248
 
137
- if (pathname === '/api/oneshot' && req.method === 'POST') {
249
+ if ((pathname === '/api/goal' || pathname === '/api/oneshot') && req.method === 'POST') {
138
250
  const body = await readBody();
139
- 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';
140
252
  const options = {
141
- targetFramework: body.targetFramework || 'vanilla',
142
- outputDir: body.outputDir || path.join(engine.rootDir, 'generated-site')
253
+ targetFramework: body.targetFramework || 'nextjs',
254
+ outputDir: body.outputDir
143
255
  };
144
- // Asynchronously start OneShot pipeline
256
+ // Asynchronously start Multi-Agent Synthesis / Goal pipeline
145
257
  engine.submitOneShotTask(prompt, options).catch(console.error);
146
- return sendJson(200, { status: 'oneshot_started', prompt, options });
258
+ return sendJson(200, { status: 'goal_started', prompt, options });
147
259
  }
148
260
 
149
- if (pathname === '/api/token-stats' && req.method === 'GET') {
150
- 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 || {
151
264
  rawTokensEstimated: 42500,
152
265
  actualTokensUsed: 11800,
153
266
  tokensSaved: 30700,
154
267
  efficiencyRatio: 72,
155
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
+ },
156
282
  strategiesActive: [
157
283
  "AST Symbol Graph Skeletonization",
158
284
  "Tiered Sliding Window Context Pruning",
@@ -164,15 +290,16 @@ export function createServer(engineOrRootDir, maybeEngine, options = {}) {
164
290
  }
165
291
 
166
292
  if (pathname === '/api/site-preview' && req.method === 'GET') {
167
- const sitePath = path.join(engine.rootDir, 'generated-site', 'index.html');
168
- try {
169
- const html = await fs.readFile(sitePath, 'utf-8');
170
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
171
- res.end(html);
172
- return;
173
- } catch {
174
- 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 {}
175
301
  }
302
+ return sendJson(404, { error: 'No generated site preview found yet. Execute a task or goal first.' });
176
303
  }
177
304
 
178
305
  if (pathname === '/api/emit' && req.method === 'POST') {
@@ -275,24 +402,118 @@ export function createServer(engineOrRootDir, maybeEngine, options = {}) {
275
402
  heartbeat.unref();
276
403
 
277
404
  let isWatching = false;
405
+ let workspaceWatcher = null;
406
+ const debouncedFiles = new Map();
278
407
 
279
- 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() {
280
432
  if (isWatching) return;
281
433
  isWatching = true;
282
434
 
283
- // Watch events.jsonl for direct external CLI / IDE writes
284
- let lastKnownEventCount = 0;
285
- 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;
286
507
  try {
287
- const initialContent = fsSync.readFileSync(engine.eventsPath, 'utf-8');
508
+ const initialContent = fsSync.readFileSync(ePath, 'utf-8');
288
509
  lastKnownEventCount = initialContent.trim().split('\n').filter(Boolean).length;
289
510
  } catch {
290
511
  lastKnownEventCount = 0;
291
512
  }
292
513
 
293
- fsSync.watchFile(engine.eventsPath, { interval: 150 }, async () => {
514
+ fsSync.watchFile(ePath, { interval: 150 }, async () => {
294
515
  try {
295
- const content = await fs.readFile(engine.eventsPath, 'utf-8');
516
+ const content = await fs.readFile(ePath, 'utf-8');
296
517
  const lines = content.trim().split('\n').filter(Boolean);
297
518
  if (lines.length > lastKnownEventCount) {
298
519
  const newLines = lines.slice(lastKnownEventCount);
@@ -301,7 +522,7 @@ export function createServer(engineOrRootDir, maybeEngine, options = {}) {
301
522
  try {
302
523
  const event = JSON.parse(line);
303
524
  engine.eventHistory.push(event);
304
- if (engine.eventHistory.length > 200) engine.eventHistory.shift();
525
+ if (engine.eventHistory.length > 300) engine.eventHistory.shift();
305
526
 
306
527
  const payload = `event: agent_event\ndata: ${JSON.stringify(event)}\n\n`;
307
528
  for (const client of sseClients) {
@@ -314,13 +535,21 @@ export function createServer(engineOrRootDir, maybeEngine, options = {}) {
314
535
  });
315
536
  }
316
537
 
317
- // Watch state.json for direct external updates
318
- if (engine.statePath) {
319
- 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 () => {
320
549
  try {
321
- const content = await fs.readFile(engine.statePath, 'utf-8');
550
+ const content = await fs.readFile(sPath, 'utf-8');
322
551
  const state = JSON.parse(content);
323
- engine.state = state;
552
+ if (engine) engine.state = state;
324
553
  const payload = `event: state_change\ndata: ${JSON.stringify({ state })}\n\n`;
325
554
  for (const client of sseClients) {
326
555
  try { client.write(payload); } catch {}
@@ -328,17 +557,31 @@ export function createServer(engineOrRootDir, maybeEngine, options = {}) {
328
557
  } catch {}
329
558
  });
330
559
  }
331
- });
560
+ }
561
+
562
+ // Start watchers when server begins listening
563
+ server.on('listening', startWatchers);
332
564
 
333
565
  server.on('close', () => {
334
566
  clearInterval(heartbeat);
335
- if (engine.eventsPath) {
336
- try { fsSync.unwatchFile(engine.eventsPath); } catch {}
567
+ isWatching = false;
568
+ if (workspaceWatcher) {
569
+ try { workspaceWatcher.close(); } catch {}
337
570
  }
338
- if (engine.statePath) {
339
- 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 {}
340
582
  }
341
583
  });
342
584
 
343
585
  return server;
344
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
+ }