@qlucent/fishi-core 0.5.0 → 0.6.0

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/dist/index.d.ts CHANGED
@@ -345,6 +345,16 @@ declare function getLearningsManagerScript(): string;
345
345
  */
346
346
  declare function getDocCheckerScript(): string;
347
347
 
348
+ /**
349
+ * Monitor Emitter Script
350
+ *
351
+ * Shared helper that other hooks can import to emit agent observability events.
352
+ * Also callable directly via CLI for one-off event emission.
353
+ *
354
+ * Zero dependencies — uses only Node.js built-ins.
355
+ */
356
+ declare function getMonitorEmitterScript(): string;
357
+
348
358
  declare function getInitCommand(): string;
349
359
 
350
360
  declare function getStatusCommand(): string;
@@ -523,4 +533,41 @@ interface McpJson {
523
533
  declare function mergeMcpJson(existing: McpJson, fishi: McpJson): McpJson;
524
534
  declare function mergeGitignore(existing: string, fishiAdditions: string): string;
525
535
 
526
- export { type AgentDefinition, type AgentRole, type AgentTemplate, type BackupManifest, type BrownfieldAnalysisData, type ClaudeMdOptions, type CommandTemplate, type ConflictCategory, type ConflictMap, type ConflictResolution, type CostMode, type DetectionCheck, type DetectionResult, type DynamicAgentConfig, type ExecutionConfig, type FileConflict, type FileResolutionMap, type FishiConfig, type FishiYamlOptions, type GateConfig, type GateStatus, type GitConfig, type HookTemplate, type InitOptions, type McpConfig, type McpServerConfig, type ModelRoutingConfig, type ModelTier, type ProjectConfig, type ProjectType, type ProjectYamlOptions, type ScaffoldOptions, type ScaffoldResult, type SkillTemplate, type StateConfig, type TaskStatus, type TaskboardConfig, type TemplateContext, architectAgentTemplate, backendAgentTemplate, createBackup, detectConflicts, devLeadTemplate, devopsAgentTemplate, docsAgentTemplate, frontendAgentTemplate, fullstackAgentTemplate, generateScaffold, getAdaptiveTaskGraphSkill, getAgentCompleteHook, getAgentFactoryTemplate, getAgentRegistryTemplate, getApiDesignSkill, getAutoCheckpointHook, getBoardCommand, getBrainstormingSkill, getBrownfieldAnalysisSkill, getBrownfieldDiscoverySkill, getClaudeMdTemplate, getCodeGenSkill, getCoordinatorFactoryTemplate, getDebuggingSkill, getDeploymentSkill, getDocCheckerScript, getDocumentationSkill, getFishiYamlTemplate, getGateCommand, getGateManagerScript, getGitignoreAdditions, getInitCommand, getLearningsManagerScript, getMasterOrchestratorTemplate, getMcpJsonTemplate, getMemoryManagerScript, getModelRoutingReference, getPhaseRunnerScript, getPostEditHook, getPrdCommand, getPrdSkill, getProjectYamlTemplate, getResetCommand, getResumeCommand, getSafetyCheckHook, getSessionStartHook, getSettingsJsonTemplate, getSprintCommand, getStatusCommand, getTaskboardOpsSkill, getTaskboardUpdateHook, getTestingSkill, getTodoManagerScript, getValidateScaffoldScript, getWorktreeManagerScript, getWorktreeSetupHook, marketingAgentTemplate, mergeClaudeMd, mergeClaudeMdTop, mergeGitignore, mergeMcpJson, mergeSettingsJson, opsLeadTemplate, planningAgentTemplate, planningLeadTemplate, qualityLeadTemplate, researchAgentTemplate, securityAgentTemplate, testingAgentTemplate, uiuxAgentTemplate, writingAgentTemplate };
536
+ interface MonitorEvent {
537
+ type: string;
538
+ agent: string;
539
+ data: Record<string, unknown>;
540
+ timestamp?: string;
541
+ }
542
+ interface MonitorSummary {
543
+ totalAgentCompletions: number;
544
+ totalFilesChanged: number;
545
+ totalTokens: number;
546
+ tokensByModel: Record<string, number>;
547
+ tokensByAgent: Record<string, number>;
548
+ toolsUsed: Record<string, number>;
549
+ dynamicAgentsCreated: number;
550
+ }
551
+ interface DynamicAgent {
552
+ name: string;
553
+ coordinator: string;
554
+ }
555
+ interface MonitorState {
556
+ events: (MonitorEvent & {
557
+ timestamp: string;
558
+ })[];
559
+ summary: MonitorSummary;
560
+ dynamicAgents: DynamicAgent[];
561
+ lastUpdated: string;
562
+ }
563
+ declare function readMonitorState(projectDir: string): MonitorState;
564
+ declare function emitEvent(projectDir: string, event: MonitorEvent): void;
565
+ declare function getAgentSummary(projectDir: string): Record<string, {
566
+ completions: number;
567
+ failures: number;
568
+ filesChanged: number;
569
+ }>;
570
+
571
+ declare function getDashboardHtml(): string;
572
+
573
+ export { type AgentDefinition, type AgentRole, type AgentTemplate, type BackupManifest, type BrownfieldAnalysisData, type ClaudeMdOptions, type CommandTemplate, type ConflictCategory, type ConflictMap, type ConflictResolution, type CostMode, type DetectionCheck, type DetectionResult, type DynamicAgent, type DynamicAgentConfig, type ExecutionConfig, type FileConflict, type FileResolutionMap, type FishiConfig, type FishiYamlOptions, type GateConfig, type GateStatus, type GitConfig, type HookTemplate, type InitOptions, type McpConfig, type McpServerConfig, type ModelRoutingConfig, type ModelTier, type MonitorEvent, type MonitorState, type MonitorSummary, type ProjectConfig, type ProjectType, type ProjectYamlOptions, type ScaffoldOptions, type ScaffoldResult, type SkillTemplate, type StateConfig, type TaskStatus, type TaskboardConfig, type TemplateContext, architectAgentTemplate, backendAgentTemplate, createBackup, detectConflicts, devLeadTemplate, devopsAgentTemplate, docsAgentTemplate, emitEvent, frontendAgentTemplate, fullstackAgentTemplate, generateScaffold, getAdaptiveTaskGraphSkill, getAgentCompleteHook, getAgentFactoryTemplate, getAgentRegistryTemplate, getAgentSummary, getApiDesignSkill, getAutoCheckpointHook, getBoardCommand, getBrainstormingSkill, getBrownfieldAnalysisSkill, getBrownfieldDiscoverySkill, getClaudeMdTemplate, getCodeGenSkill, getCoordinatorFactoryTemplate, getDashboardHtml, getDebuggingSkill, getDeploymentSkill, getDocCheckerScript, getDocumentationSkill, getFishiYamlTemplate, getGateCommand, getGateManagerScript, getGitignoreAdditions, getInitCommand, getLearningsManagerScript, getMasterOrchestratorTemplate, getMcpJsonTemplate, getMemoryManagerScript, getModelRoutingReference, getMonitorEmitterScript, getPhaseRunnerScript, getPostEditHook, getPrdCommand, getPrdSkill, getProjectYamlTemplate, getResetCommand, getResumeCommand, getSafetyCheckHook, getSessionStartHook, getSettingsJsonTemplate, getSprintCommand, getStatusCommand, getTaskboardOpsSkill, getTaskboardUpdateHook, getTestingSkill, getTodoManagerScript, getValidateScaffoldScript, getWorktreeManagerScript, getWorktreeSetupHook, marketingAgentTemplate, mergeClaudeMd, mergeClaudeMdTop, mergeGitignore, mergeMcpJson, mergeSettingsJson, opsLeadTemplate, planningAgentTemplate, planningLeadTemplate, qualityLeadTemplate, readMonitorState, researchAgentTemplate, securityAgentTemplate, testingAgentTemplate, uiuxAgentTemplate, writingAgentTemplate };
package/dist/index.js CHANGED
@@ -8217,6 +8217,183 @@ try {
8217
8217
  `;
8218
8218
  }
8219
8219
 
8220
+ // src/templates/hooks/monitor-emitter.ts
8221
+ function getMonitorEmitterScript() {
8222
+ return `#!/usr/bin/env node
8223
+ // monitor-emitter.mjs \u2014 FISHI agent observability event emitter
8224
+ //
8225
+ // Usage as a library (ESM import in another .mjs hook):
8226
+ // const { emitMonitorEvent } = await import('./.fishi/scripts/monitor-emitter.mjs');
8227
+ // await emitMonitorEvent(process.cwd(), { type: 'agent.completed', agent: 'backend-agent', data: { filesChanged: 3 } });
8228
+ //
8229
+ // Usage via CLI:
8230
+ // node .fishi/scripts/monitor-emitter.mjs '{"type":"agent.completed","agent":"backend-agent","data":{"filesChanged":3}}'
8231
+
8232
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
8233
+ import { join, dirname } from 'path';
8234
+
8235
+ // \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8236
+
8237
+ const MAX_EVENTS = 500;
8238
+
8239
+ // \u2500\u2500 State helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8240
+
8241
+ function monitorPath(root) {
8242
+ return join(root, '.fishi', 'state', 'monitor.json');
8243
+ }
8244
+
8245
+ function emptySummary() {
8246
+ return {
8247
+ totalAgentCompletions: 0,
8248
+ totalFilesChanged: 0,
8249
+ totalTokens: 0,
8250
+ tokensByModel: {},
8251
+ tokensByAgent: {},
8252
+ toolsUsed: {},
8253
+ dynamicAgentsCreated: 0,
8254
+ };
8255
+ }
8256
+
8257
+ function emptyState() {
8258
+ return {
8259
+ events: [],
8260
+ summary: emptySummary(),
8261
+ dynamicAgents: [],
8262
+ lastUpdated: new Date().toISOString(),
8263
+ };
8264
+ }
8265
+
8266
+ function readState(root) {
8267
+ const p = monitorPath(root);
8268
+ if (!existsSync(p)) return emptyState();
8269
+ try {
8270
+ return JSON.parse(readFileSync(p, 'utf-8'));
8271
+ } catch {
8272
+ return emptyState();
8273
+ }
8274
+ }
8275
+
8276
+ function writeState(root, state) {
8277
+ const p = monitorPath(root);
8278
+ const dir = dirname(p);
8279
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
8280
+ writeFileSync(p, JSON.stringify(state, null, 2) + '\\n', 'utf-8');
8281
+ }
8282
+
8283
+ // \u2500\u2500 Summary updater \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8284
+
8285
+ function updateSummary(state, event) {
8286
+ switch (event.type) {
8287
+ case 'agent.completed': {
8288
+ state.summary.totalAgentCompletions++;
8289
+ const files = (event.data && typeof event.data.filesChanged === 'number')
8290
+ ? event.data.filesChanged
8291
+ : 0;
8292
+ state.summary.totalFilesChanged += files;
8293
+ break;
8294
+ }
8295
+ case 'tokens.used': {
8296
+ const input = (event.data && typeof event.data.inputTokens === 'number')
8297
+ ? event.data.inputTokens
8298
+ : 0;
8299
+ const output = (event.data && typeof event.data.outputTokens === 'number')
8300
+ ? event.data.outputTokens
8301
+ : 0;
8302
+ const total = input + output;
8303
+ const model = (event.data && event.data.model) || 'unknown';
8304
+ state.summary.totalTokens += total;
8305
+ state.summary.tokensByModel[model] = (state.summary.tokensByModel[model] || 0) + total;
8306
+ state.summary.tokensByAgent[event.agent] =
8307
+ (state.summary.tokensByAgent[event.agent] || 0) + total;
8308
+ break;
8309
+ }
8310
+ case 'tool.used': {
8311
+ const tool = event.data && event.data.tool;
8312
+ if (tool) {
8313
+ state.summary.toolsUsed[tool] = (state.summary.toolsUsed[tool] || 0) + 1;
8314
+ }
8315
+ break;
8316
+ }
8317
+ case 'agent.created': {
8318
+ if (event.data && event.data.dynamic) {
8319
+ state.summary.dynamicAgentsCreated++;
8320
+ state.dynamicAgents.push({
8321
+ name: event.agent,
8322
+ coordinator: (event.data.coordinator) || 'unknown',
8323
+ });
8324
+ }
8325
+ break;
8326
+ }
8327
+ }
8328
+ }
8329
+
8330
+ // \u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8331
+
8332
+ /**
8333
+ * Emit a monitor event.
8334
+ *
8335
+ * @param {string} root - Project root directory (where .fishi/ lives)
8336
+ * @param {{ type: string, agent: string, data: object, timestamp?: string }} event
8337
+ */
8338
+ export function emitMonitorEvent(root, event) {
8339
+ const state = readState(root);
8340
+
8341
+ const timestamped = {
8342
+ ...event,
8343
+ timestamp: event.timestamp || new Date().toISOString(),
8344
+ };
8345
+
8346
+ state.events.push(timestamped);
8347
+ if (state.events.length > MAX_EVENTS) {
8348
+ state.events = state.events.slice(-MAX_EVENTS);
8349
+ }
8350
+
8351
+ updateSummary(state, event);
8352
+
8353
+ state.lastUpdated = new Date().toISOString();
8354
+ writeState(root, state);
8355
+ }
8356
+
8357
+ // \u2500\u2500 CLI entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8358
+
8359
+ const isMain = process.argv[1] &&
8360
+ (process.argv[1].endsWith('monitor-emitter.mjs') || process.argv[1].endsWith('monitor-emitter'));
8361
+
8362
+ if (isMain) {
8363
+ const raw = process.argv[2];
8364
+ if (!raw) {
8365
+ console.error('[FISHI] Usage: node .fishi/scripts/monitor-emitter.mjs '{"type":"...","agent":"...","data":{...}}'');
8366
+ process.exit(1);
8367
+ }
8368
+
8369
+ let event;
8370
+ try {
8371
+ event = JSON.parse(raw);
8372
+ } catch (err) {
8373
+ console.error('[FISHI] monitor-emitter: invalid JSON:', err.message);
8374
+ process.exit(1);
8375
+ }
8376
+
8377
+ if (!event.type || !event.agent) {
8378
+ console.error('[FISHI] monitor-emitter: event must have "type" and "agent" fields');
8379
+ process.exit(1);
8380
+ }
8381
+
8382
+ if (!event.data || typeof event.data !== 'object') {
8383
+ event.data = {};
8384
+ }
8385
+
8386
+ try {
8387
+ emitMonitorEvent(process.cwd(), event);
8388
+ console.log('[FISHI] monitor event emitted:', event.type, '/', event.agent);
8389
+ } catch (err) {
8390
+ console.error('[FISHI] monitor-emitter error:', err.message);
8391
+ process.exit(1);
8392
+ }
8393
+ }
8394
+ `;
8395
+ }
8396
+
8220
8397
  // src/templates/commands/init-command.ts
8221
8398
  function getInitCommand() {
8222
8399
  return `# /fishi-init \u2014 Launch the FISHI Orchestration Pipeline
@@ -10585,7 +10762,8 @@ async function generateScaffold(targetDir, options) {
10585
10762
  await write(".fishi/scripts/memory-manager.mjs", getMemoryManagerScript());
10586
10763
  await write(".fishi/scripts/learnings-manager.mjs", getLearningsManagerScript());
10587
10764
  await write(".fishi/scripts/doc-checker.mjs", getDocCheckerScript());
10588
- const hookCount = 15;
10765
+ await write(".fishi/scripts/monitor-emitter.mjs", getMonitorEmitterScript());
10766
+ const hookCount = 16;
10589
10767
  const todoTemplate = (name) => `# TODO \u2014 ${name}
10590
10768
 
10591
10769
  ## Active
@@ -10670,6 +10848,12 @@ async function generateScaffold(targetDir, options) {
10670
10848
  await write(".fishi/state/agent-registry.yaml", getAgentRegistryTemplate());
10671
10849
  await write(".fishi/state/task-graph.yaml", "tasks: []\ndependencies: []\n");
10672
10850
  await write(".fishi/state/gates.yaml", "gates: []\n");
10851
+ await write(".fishi/state/monitor.json", JSON.stringify({
10852
+ events: [],
10853
+ summary: { totalAgentCompletions: 0, totalFilesChanged: 0, totalTokens: 0, tokensByModel: {}, tokensByAgent: {}, toolsUsed: {}, dynamicAgentsCreated: 0 },
10854
+ dynamicAgents: [],
10855
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
10856
+ }, null, 2) + "\n");
10673
10857
  await write(".fishi/mcp-registry.yaml", getMcpRegistryTemplate());
10674
10858
  await write(".fishi/model-routing.md", getModelRoutingReference());
10675
10859
  await write(".fishi/memory/project-context.md", getProjectContextTemplate(ctx));
@@ -10927,7 +11111,7 @@ async function createBackup(targetDir, conflictingFiles) {
10927
11111
  manifestFiles.push({ path: relPath, size: stat.size });
10928
11112
  }
10929
11113
  }
10930
- const fishiVersion = "0.5.0";
11114
+ const fishiVersion = "0.6.0";
10931
11115
  const manifest = {
10932
11116
  timestamp: now.toISOString(),
10933
11117
  fishi_version: fishiVersion,
@@ -10939,6 +11123,444 @@ async function createBackup(targetDir, conflictingFiles) {
10939
11123
  await rename(tmpPath, manifestPath);
10940
11124
  return backupDir;
10941
11125
  }
11126
+
11127
+ // src/generators/monitor.ts
11128
+ import { existsSync as existsSync4, readFileSync, writeFileSync, mkdirSync } from "fs";
11129
+ import { join as join4, dirname as dirname3 } from "path";
11130
+ var MAX_EVENTS = 500;
11131
+ function monitorPath(projectDir) {
11132
+ return join4(projectDir, ".fishi", "state", "monitor.json");
11133
+ }
11134
+ function emptySummary() {
11135
+ return {
11136
+ totalAgentCompletions: 0,
11137
+ totalFilesChanged: 0,
11138
+ totalTokens: 0,
11139
+ tokensByModel: {},
11140
+ tokensByAgent: {},
11141
+ toolsUsed: {},
11142
+ dynamicAgentsCreated: 0
11143
+ };
11144
+ }
11145
+ function emptyState() {
11146
+ return {
11147
+ events: [],
11148
+ summary: emptySummary(),
11149
+ dynamicAgents: [],
11150
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
11151
+ };
11152
+ }
11153
+ function readMonitorState(projectDir) {
11154
+ const p = monitorPath(projectDir);
11155
+ if (!existsSync4(p)) return emptyState();
11156
+ try {
11157
+ return JSON.parse(readFileSync(p, "utf-8"));
11158
+ } catch {
11159
+ return emptyState();
11160
+ }
11161
+ }
11162
+ function emitEvent(projectDir, event) {
11163
+ const state = readMonitorState(projectDir);
11164
+ const timestamped = { ...event, timestamp: event.timestamp || (/* @__PURE__ */ new Date()).toISOString() };
11165
+ state.events.push(timestamped);
11166
+ if (state.events.length > MAX_EVENTS) {
11167
+ state.events = state.events.slice(-MAX_EVENTS);
11168
+ }
11169
+ switch (event.type) {
11170
+ case "agent.completed": {
11171
+ state.summary.totalAgentCompletions++;
11172
+ const files = event.data.filesChanged || 0;
11173
+ state.summary.totalFilesChanged += files;
11174
+ break;
11175
+ }
11176
+ case "tokens.used": {
11177
+ const input = event.data.inputTokens || 0;
11178
+ const output = event.data.outputTokens || 0;
11179
+ const total = input + output;
11180
+ const model = event.data.model || "unknown";
11181
+ state.summary.totalTokens += total;
11182
+ state.summary.tokensByModel[model] = (state.summary.tokensByModel[model] || 0) + total;
11183
+ state.summary.tokensByAgent[event.agent] = (state.summary.tokensByAgent[event.agent] || 0) + total;
11184
+ break;
11185
+ }
11186
+ case "tool.used": {
11187
+ const tool = event.data.tool;
11188
+ state.summary.toolsUsed[tool] = (state.summary.toolsUsed[tool] || 0) + 1;
11189
+ break;
11190
+ }
11191
+ case "agent.created": {
11192
+ if (event.data.dynamic) {
11193
+ state.summary.dynamicAgentsCreated++;
11194
+ state.dynamicAgents.push({
11195
+ name: event.agent,
11196
+ coordinator: event.data.coordinator || "unknown"
11197
+ });
11198
+ }
11199
+ break;
11200
+ }
11201
+ }
11202
+ state.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
11203
+ const dir = dirname3(monitorPath(projectDir));
11204
+ if (!existsSync4(dir)) mkdirSync(dir, { recursive: true });
11205
+ writeFileSync(monitorPath(projectDir), JSON.stringify(state, null, 2) + "\n", "utf-8");
11206
+ }
11207
+ function getAgentSummary(projectDir) {
11208
+ const state = readMonitorState(projectDir);
11209
+ const agents = {};
11210
+ for (const event of state.events) {
11211
+ if (event.type === "agent.completed") {
11212
+ if (!agents[event.agent]) agents[event.agent] = { completions: 0, failures: 0, filesChanged: 0 };
11213
+ agents[event.agent].completions++;
11214
+ if (event.data.status === "failed") agents[event.agent].failures++;
11215
+ agents[event.agent].filesChanged += event.data.filesChanged || 0;
11216
+ }
11217
+ }
11218
+ return agents;
11219
+ }
11220
+
11221
+ // src/templates/dashboard/index-html.ts
11222
+ function getDashboardHtml() {
11223
+ return `<!DOCTYPE html>
11224
+ <html lang="en">
11225
+ <head>
11226
+ <meta charset="UTF-8" />
11227
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
11228
+ <title>FISHI Agent Dashboard</title>
11229
+ <style>
11230
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
11231
+
11232
+ body {
11233
+ background: #0a0a0f;
11234
+ color: #e2e8f0;
11235
+ font-family: 'Courier New', Courier, monospace;
11236
+ font-size: 14px;
11237
+ min-height: 100vh;
11238
+ }
11239
+
11240
+ /* \u2500\u2500 Header \u2500\u2500 */
11241
+ header {
11242
+ display: flex;
11243
+ align-items: center;
11244
+ justify-content: space-between;
11245
+ padding: 14px 24px;
11246
+ background: #12121a;
11247
+ border-bottom: 1px solid #1e1e2e;
11248
+ }
11249
+ header h1 { font-size: 18px; color: #06b6d4; letter-spacing: 0.05em; }
11250
+ .status-dot {
11251
+ width: 10px; height: 10px;
11252
+ border-radius: 50%;
11253
+ background: #22c55e;
11254
+ display: inline-block;
11255
+ margin-right: 8px;
11256
+ animation: pulse 2s infinite;
11257
+ }
11258
+ .status-dot.offline { background: #ef4444; animation: none; }
11259
+ @keyframes pulse {
11260
+ 0%, 100% { opacity: 1; }
11261
+ 50% { opacity: 0.4; }
11262
+ }
11263
+ .last-updated { color: #6b7280; font-size: 12px; }
11264
+
11265
+ /* \u2500\u2500 Layout \u2500\u2500 */
11266
+ main { padding: 20px 24px; }
11267
+
11268
+ /* \u2500\u2500 Stat cards \u2500\u2500 */
11269
+ .stat-grid {
11270
+ display: grid;
11271
+ grid-template-columns: repeat(4, 1fr);
11272
+ gap: 12px;
11273
+ margin-bottom: 20px;
11274
+ }
11275
+ @media (max-width: 900px) { .stat-grid { grid-template-columns: repeat(2, 1fr); } }
11276
+ .card {
11277
+ background: #12121a;
11278
+ border: 1px solid #1e1e2e;
11279
+ border-radius: 8px;
11280
+ padding: 16px;
11281
+ }
11282
+ .card-label { color: #6b7280; font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 8px; }
11283
+ .card-value { font-size: 28px; color: #06b6d4; font-weight: bold; }
11284
+
11285
+ /* \u2500\u2500 Phase bar \u2500\u2500 */
11286
+ .phase-section { margin-bottom: 20px; }
11287
+ .phase-bar {
11288
+ display: flex;
11289
+ gap: 4px;
11290
+ flex-wrap: wrap;
11291
+ background: #12121a;
11292
+ border: 1px solid #1e1e2e;
11293
+ border-radius: 8px;
11294
+ padding: 12px 16px;
11295
+ }
11296
+ .phase-pill {
11297
+ padding: 4px 12px;
11298
+ border-radius: 4px;
11299
+ font-size: 12px;
11300
+ color: #6b7280;
11301
+ border: 1px solid #2d2d3d;
11302
+ background: #0a0a0f;
11303
+ }
11304
+ .phase-pill.done { color: #22c55e; border-color: #22c55e; background: rgba(34,197,94,0.08); }
11305
+ .phase-pill.active { color: #06b6d4; border-color: #06b6d4; background: rgba(6,182,212,0.12); font-weight: bold; }
11306
+ .phase-arrow { color: #2d2d3d; align-self: center; }
11307
+
11308
+ /* \u2500\u2500 Main grid \u2500\u2500 */
11309
+ .main-grid {
11310
+ display: grid;
11311
+ grid-template-columns: 1fr 1fr;
11312
+ gap: 16px;
11313
+ margin-bottom: 16px;
11314
+ }
11315
+ @media (max-width: 800px) { .main-grid { grid-template-columns: 1fr; } }
11316
+
11317
+ .panel {
11318
+ background: #12121a;
11319
+ border: 1px solid #1e1e2e;
11320
+ border-radius: 8px;
11321
+ padding: 16px;
11322
+ }
11323
+ .panel-title {
11324
+ color: #94a3b8;
11325
+ font-size: 12px;
11326
+ text-transform: uppercase;
11327
+ letter-spacing: 0.1em;
11328
+ margin-bottom: 12px;
11329
+ border-bottom: 1px solid #1e1e2e;
11330
+ padding-bottom: 8px;
11331
+ }
11332
+
11333
+ /* \u2500\u2500 Events \u2500\u2500 */
11334
+ .event-row {
11335
+ display: flex;
11336
+ gap: 10px;
11337
+ padding: 5px 0;
11338
+ border-bottom: 1px solid #1a1a28;
11339
+ font-size: 12px;
11340
+ }
11341
+ .event-row:last-child { border-bottom: none; }
11342
+ .event-ts { color: #4b5563; min-width: 80px; }
11343
+ .event-type { color: #06b6d4; min-width: 130px; }
11344
+ .event-agent { color: #a78bfa; }
11345
+ .empty-msg { color: #4b5563; font-style: italic; }
11346
+
11347
+ /* \u2500\u2500 Agent activity \u2500\u2500 */
11348
+ .agent-row {
11349
+ display: flex;
11350
+ justify-content: space-between;
11351
+ padding: 6px 0;
11352
+ border-bottom: 1px solid #1a1a28;
11353
+ font-size: 12px;
11354
+ }
11355
+ .agent-row:last-child { border-bottom: none; }
11356
+ .agent-name { color: #a78bfa; }
11357
+ .agent-stats { color: #6b7280; display: flex; gap: 12px; }
11358
+ .stat-ok { color: #22c55e; }
11359
+ .stat-fail { color: #ef4444; }
11360
+ .stat-files { color: #06b6d4; }
11361
+
11362
+ /* \u2500\u2500 Bottom row \u2500\u2500 */
11363
+ .bottom-grid {
11364
+ display: grid;
11365
+ grid-template-columns: 1fr 1fr 1fr;
11366
+ gap: 16px;
11367
+ }
11368
+ @media (max-width: 900px) { .bottom-grid { grid-template-columns: 1fr; } }
11369
+
11370
+ /* \u2500\u2500 KV table \u2500\u2500 */
11371
+ .kv-row {
11372
+ display: flex;
11373
+ justify-content: space-between;
11374
+ padding: 4px 0;
11375
+ border-bottom: 1px solid #1a1a28;
11376
+ font-size: 12px;
11377
+ }
11378
+ .kv-row:last-child { border-bottom: none; }
11379
+ .kv-key { color: #fbbf24; }
11380
+ .kv-val { color: #06b6d4; }
11381
+
11382
+ /* \u2500\u2500 Gates \u2500\u2500 */
11383
+ .gate-row {
11384
+ display: flex;
11385
+ align-items: center;
11386
+ gap: 8px;
11387
+ padding: 4px 0;
11388
+ font-size: 12px;
11389
+ border-bottom: 1px solid #1a1a28;
11390
+ }
11391
+ .gate-row:last-child { border-bottom: none; }
11392
+ .gate-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
11393
+ .gate-dot.passed { background: #22c55e; }
11394
+ .gate-dot.failed { background: #ef4444; }
11395
+ .gate-dot.pending { background: #f59e0b; }
11396
+ .gate-name { color: #e2e8f0; flex: 1; }
11397
+ .gate-status { font-size: 11px; }
11398
+ .gate-status.passed { color: #22c55e; }
11399
+ .gate-status.failed { color: #ef4444; }
11400
+ .gate-status.pending { color: #f59e0b; }
11401
+ </style>
11402
+ </head>
11403
+ <body>
11404
+ <header>
11405
+ <h1><span class="status-dot" id="statusDot"></span>FISHI Agent Dashboard</h1>
11406
+ <span class="last-updated" id="lastUpdated">Loading...</span>
11407
+ </header>
11408
+ <main>
11409
+ <!-- Stat cards -->
11410
+ <div class="stat-grid">
11411
+ <div class="card"><div class="card-label">Agent Completions</div><div class="card-value" id="statCompletions">\u2014</div></div>
11412
+ <div class="card"><div class="card-label">Files Changed</div><div class="card-value" id="statFiles">\u2014</div></div>
11413
+ <div class="card"><div class="card-label">Total Tokens</div><div class="card-value" id="statTokens">\u2014</div></div>
11414
+ <div class="card"><div class="card-label">Dynamic Agents</div><div class="card-value" id="statDynamic">\u2014</div></div>
11415
+ </div>
11416
+
11417
+ <!-- Phase bar -->
11418
+ <div class="phase-section">
11419
+ <div class="phase-bar" id="phaseBar">Loading phases...</div>
11420
+ </div>
11421
+
11422
+ <!-- Main grid -->
11423
+ <div class="main-grid">
11424
+ <div class="panel">
11425
+ <div class="panel-title">Recent Events</div>
11426
+ <div id="eventsPanel"><span class="empty-msg">No events yet.</span></div>
11427
+ </div>
11428
+ <div class="panel">
11429
+ <div class="panel-title">Agent Activity</div>
11430
+ <div id="agentsPanel"><span class="empty-msg">No agent activity yet.</span></div>
11431
+ </div>
11432
+ </div>
11433
+
11434
+ <!-- Bottom row -->
11435
+ <div class="bottom-grid">
11436
+ <div class="panel">
11437
+ <div class="panel-title">Tokens by Model</div>
11438
+ <div id="tokensPanel"><span class="empty-msg">No data.</span></div>
11439
+ </div>
11440
+ <div class="panel">
11441
+ <div class="panel-title">Tools Used</div>
11442
+ <div id="toolsPanel"><span class="empty-msg">No data.</span></div>
11443
+ </div>
11444
+ <div class="panel">
11445
+ <div class="panel-title">Gates</div>
11446
+ <div id="gatesPanel"><span class="empty-msg">No gates.</span></div>
11447
+ </div>
11448
+ </div>
11449
+ </main>
11450
+
11451
+ <script>
11452
+ const PHASES = ['init', 'discovery', 'planning', 'development', 'testing', 'review', 'deployed'];
11453
+
11454
+ function fmt(n) {
11455
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
11456
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
11457
+ return String(n);
11458
+ }
11459
+
11460
+ function fmtTime(iso) {
11461
+ try { return new Date(iso).toLocaleTimeString(); } catch { return iso; }
11462
+ }
11463
+
11464
+ function renderPhase(currentPhase) {
11465
+ const idx = PHASES.indexOf(currentPhase);
11466
+ const bar = document.getElementById('phaseBar');
11467
+ let html = '';
11468
+ PHASES.forEach((p, i) => {
11469
+ if (i > 0) html += '<span class="phase-arrow">&#8594;</span>';
11470
+ const cls = i < idx ? 'done' : i === idx ? 'active' : '';
11471
+ html += \`<span class="phase-pill \${cls}">\${p}</span>\`;
11472
+ });
11473
+ bar.innerHTML = html;
11474
+ }
11475
+
11476
+ function renderEvents(events) {
11477
+ const panel = document.getElementById('eventsPanel');
11478
+ const recent = events.slice(-10).reverse();
11479
+ if (!recent.length) { panel.innerHTML = '<span class="empty-msg">No events yet.</span>'; return; }
11480
+ panel.innerHTML = recent.map(ev =>
11481
+ \`<div class="event-row">
11482
+ <span class="event-ts">\${fmtTime(ev.timestamp)}</span>
11483
+ <span class="event-type">\${ev.type}</span>
11484
+ <span class="event-agent">\${ev.agent}</span>
11485
+ </div>\`
11486
+ ).join('');
11487
+ }
11488
+
11489
+ function renderAgents(agentSummary) {
11490
+ const panel = document.getElementById('agentsPanel');
11491
+ const entries = Object.entries(agentSummary);
11492
+ if (!entries.length) { panel.innerHTML = '<span class="empty-msg">No agent activity yet.</span>'; return; }
11493
+ entries.sort((a, b) => b[1].completions - a[1].completions);
11494
+ panel.innerHTML = entries.map(([name, s]) =>
11495
+ \`<div class="agent-row">
11496
+ <span class="agent-name">\${name}</span>
11497
+ <span class="agent-stats">
11498
+ <span class="stat-ok">&#10003; \${s.completions}</span>
11499
+ <span class="stat-fail">\${s.failures > 0 ? '&#10007; ' + s.failures : ''}</span>
11500
+ <span class="stat-files">&#128196; \${s.filesChanged}</span>
11501
+ </span>
11502
+ </div>\`
11503
+ ).join('');
11504
+ }
11505
+
11506
+ function renderKV(data, panelId, keyClass) {
11507
+ const panel = document.getElementById(panelId);
11508
+ const entries = Object.entries(data).sort((a, b) => b[1] - a[1]);
11509
+ if (!entries.length) { panel.innerHTML = '<span class="empty-msg">No data.</span>'; return; }
11510
+ panel.innerHTML = entries.map(([k, v]) =>
11511
+ \`<div class="kv-row"><span class="kv-key">\${k}</span><span class="kv-val">\${fmt(v)}</span></div>\`
11512
+ ).join('');
11513
+ }
11514
+
11515
+ function renderGates(gates) {
11516
+ const panel = document.getElementById('gatesPanel');
11517
+ if (!gates || !gates.length) { panel.innerHTML = '<span class="empty-msg">No gates.</span>'; return; }
11518
+ panel.innerHTML = gates.map(g => {
11519
+ const cls = g.status === 'passed' ? 'passed' : g.status === 'failed' ? 'failed' : 'pending';
11520
+ return \`<div class="gate-row">
11521
+ <span class="gate-dot \${cls}"></span>
11522
+ <span class="gate-name">\${g.name}</span>
11523
+ <span class="gate-status \${cls}">\${g.status}</span>
11524
+ </div>\`;
11525
+ }).join('');
11526
+ }
11527
+
11528
+ async function fetchAndRender() {
11529
+ const dot = document.getElementById('statusDot');
11530
+ const lu = document.getElementById('lastUpdated');
11531
+ try {
11532
+ const res = await fetch('/api/state');
11533
+ if (!res.ok) throw new Error('HTTP ' + res.status);
11534
+ const data = await res.json();
11535
+
11536
+ dot.classList.remove('offline');
11537
+
11538
+ const s = data.summary || {};
11539
+ document.getElementById('statCompletions').textContent = fmt(s.totalAgentCompletions || 0);
11540
+ document.getElementById('statFiles').textContent = fmt(s.totalFilesChanged || 0);
11541
+ document.getElementById('statTokens').textContent = fmt(s.totalTokens || 0);
11542
+ document.getElementById('statDynamic').textContent = fmt(s.dynamicAgentsCreated || 0);
11543
+
11544
+ renderPhase(data.phase || 'init');
11545
+ renderEvents(data.events || []);
11546
+ renderAgents(data.agentSummary || {});
11547
+ renderKV(s.tokensByModel || {}, 'tokensPanel');
11548
+ renderKV(s.toolsUsed || {}, 'toolsPanel');
11549
+ renderGates(data.gates || []);
11550
+
11551
+ lu.textContent = 'Updated: ' + new Date().toLocaleTimeString();
11552
+ } catch (err) {
11553
+ dot.classList.add('offline');
11554
+ lu.textContent = 'Error: ' + err.message;
11555
+ }
11556
+ }
11557
+
11558
+ fetchAndRender();
11559
+ setInterval(fetchAndRender, 2000);
11560
+ </script>
11561
+ </body>
11562
+ </html>`;
11563
+ }
10942
11564
  export {
10943
11565
  architectAgentTemplate,
10944
11566
  backendAgentTemplate,
@@ -10947,6 +11569,7 @@ export {
10947
11569
  devLeadTemplate,
10948
11570
  devopsAgentTemplate,
10949
11571
  docsAgentTemplate,
11572
+ emitEvent,
10950
11573
  frontendAgentTemplate,
10951
11574
  fullstackAgentTemplate,
10952
11575
  generateScaffold,
@@ -10954,6 +11577,7 @@ export {
10954
11577
  getAgentCompleteHook,
10955
11578
  getAgentFactoryTemplate,
10956
11579
  getAgentRegistryTemplate,
11580
+ getAgentSummary,
10957
11581
  getApiDesignSkill,
10958
11582
  getAutoCheckpointHook,
10959
11583
  getBoardCommand,
@@ -10963,6 +11587,7 @@ export {
10963
11587
  getClaudeMdTemplate,
10964
11588
  getCodeGenSkill,
10965
11589
  getCoordinatorFactoryTemplate,
11590
+ getDashboardHtml,
10966
11591
  getDebuggingSkill,
10967
11592
  getDeploymentSkill,
10968
11593
  getDocCheckerScript,
@@ -10977,6 +11602,7 @@ export {
10977
11602
  getMcpJsonTemplate,
10978
11603
  getMemoryManagerScript,
10979
11604
  getModelRoutingReference,
11605
+ getMonitorEmitterScript,
10980
11606
  getPhaseRunnerScript,
10981
11607
  getPostEditHook,
10982
11608
  getPrdCommand,
@@ -11006,6 +11632,7 @@ export {
11006
11632
  planningAgentTemplate,
11007
11633
  planningLeadTemplate,
11008
11634
  qualityLeadTemplate,
11635
+ readMonitorState,
11009
11636
  researchAgentTemplate,
11010
11637
  securityAgentTemplate,
11011
11638
  testingAgentTemplate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qlucent/fishi-core",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Shared templates, types, and generators for the FISHI framework",
5
5
  "license": "MIT",
6
6
  "type": "module",