@qlucent/fishi-core 0.5.0 → 0.7.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,97 @@ 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
+ type SandboxMode = 'docker' | 'process';
572
+ interface SandboxConfig {
573
+ mode: SandboxMode;
574
+ dockerAvailable: boolean;
575
+ }
576
+ interface SandboxPolicy {
577
+ networkAllow: string[];
578
+ envPassthrough: string[];
579
+ timeout: number;
580
+ memory: string;
581
+ cpus: number;
582
+ }
583
+ interface SandboxRunResult {
584
+ stdout: string;
585
+ stderr: string;
586
+ exitCode: number;
587
+ timedOut: boolean;
588
+ }
589
+ /**
590
+ * Detect if Docker is installed and running.
591
+ */
592
+ declare function detectDocker(): boolean;
593
+ /**
594
+ * Read sandbox config from .fishi/fishi.yaml
595
+ */
596
+ declare function readSandboxConfig(projectDir: string): SandboxConfig;
597
+ /**
598
+ * Read sandbox policy from .fishi/sandbox-policy.yaml, or return defaults.
599
+ */
600
+ declare function readSandboxPolicy(projectDir: string): SandboxPolicy;
601
+ /**
602
+ * Build a restricted env object for process mode.
603
+ * Strips all env vars except explicitly allowed ones + essentials.
604
+ */
605
+ declare function buildSandboxEnv(policy: SandboxPolicy): Record<string, string>;
606
+ /**
607
+ * Run a command in process-mode sandbox.
608
+ */
609
+ declare function runInProcessSandbox(command: string, args: string[], worktreePath: string, policy: SandboxPolicy): SandboxRunResult;
610
+ /**
611
+ * Run a command in Docker sandbox.
612
+ */
613
+ declare function runInDockerSandbox(command: string, args: string[], worktreePath: string, policy: SandboxPolicy, options?: {
614
+ nodeModulesPath?: string;
615
+ }): SandboxRunResult;
616
+ /**
617
+ * Run a command in the configured sandbox mode.
618
+ */
619
+ declare function runInSandbox(command: string, args: string[], worktreePath: string, projectDir: string, options?: {
620
+ nodeModulesPath?: string;
621
+ }): SandboxRunResult;
622
+
623
+ declare function getSandboxPolicyTemplate(): string;
624
+
625
+ declare function getDockerfileTemplate(): string;
626
+
627
+ declare function getDashboardHtml(): string;
628
+
629
+ 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 SandboxConfig, type SandboxMode, type SandboxPolicy, type SandboxRunResult, type ScaffoldOptions, type ScaffoldResult, type SkillTemplate, type StateConfig, type TaskStatus, type TaskboardConfig, type TemplateContext, architectAgentTemplate, backendAgentTemplate, buildSandboxEnv, createBackup, detectConflicts, detectDocker, 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, getDockerfileTemplate, getDocumentationSkill, getFishiYamlTemplate, getGateCommand, getGateManagerScript, getGitignoreAdditions, getInitCommand, getLearningsManagerScript, getMasterOrchestratorTemplate, getMcpJsonTemplate, getMemoryManagerScript, getModelRoutingReference, getMonitorEmitterScript, getPhaseRunnerScript, getPostEditHook, getPrdCommand, getPrdSkill, getProjectYamlTemplate, getResetCommand, getResumeCommand, getSafetyCheckHook, getSandboxPolicyTemplate, getSessionStartHook, getSettingsJsonTemplate, getSprintCommand, getStatusCommand, getTaskboardOpsSkill, getTaskboardUpdateHook, getTestingSkill, getTodoManagerScript, getValidateScaffoldScript, getWorktreeManagerScript, getWorktreeSetupHook, marketingAgentTemplate, mergeClaudeMd, mergeClaudeMdTop, mergeGitignore, mergeMcpJson, mergeSettingsJson, opsLeadTemplate, planningAgentTemplate, planningLeadTemplate, qualityLeadTemplate, readMonitorState, readSandboxConfig, readSandboxPolicy, researchAgentTemplate, runInDockerSandbox, runInProcessSandbox, runInSandbox, 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.7.0";
10931
11115
  const manifest = {
10932
11116
  timestamp: now.toISOString(),
10933
11117
  fishi_version: fishiVersion,
@@ -10939,14 +11123,653 @@ 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/generators/sandbox.ts
11222
+ import { execSync, execFileSync } from "child_process";
11223
+ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
11224
+ import { join as join5 } from "path";
11225
+ var DEFAULT_POLICY = {
11226
+ networkAllow: ["registry.npmjs.org", "localhost", "127.0.0.1"],
11227
+ envPassthrough: [],
11228
+ timeout: 600,
11229
+ // 10 minutes
11230
+ memory: "2g",
11231
+ cpus: 2
11232
+ };
11233
+ function detectDocker() {
11234
+ try {
11235
+ execSync("docker info", { stdio: "pipe", timeout: 5e3 });
11236
+ return true;
11237
+ } catch {
11238
+ return false;
11239
+ }
11240
+ }
11241
+ function readSandboxConfig(projectDir) {
11242
+ const yamlPath = join5(projectDir, ".fishi", "fishi.yaml");
11243
+ if (!existsSync5(yamlPath)) {
11244
+ return { mode: "process", dockerAvailable: false };
11245
+ }
11246
+ const content = readFileSync2(yamlPath, "utf-8");
11247
+ const modeMatch = content.match(/^\s*mode:\s*(docker|process)/m);
11248
+ const dockerMatch = content.match(/^\s*docker_available:\s*(true|false)/m);
11249
+ return {
11250
+ mode: modeMatch?.[1] || "process",
11251
+ dockerAvailable: dockerMatch?.[1] === "true"
11252
+ };
11253
+ }
11254
+ function readSandboxPolicy(projectDir) {
11255
+ const policyPath = join5(projectDir, ".fishi", "sandbox-policy.yaml");
11256
+ if (!existsSync5(policyPath)) return { ...DEFAULT_POLICY };
11257
+ const content = readFileSync2(policyPath, "utf-8");
11258
+ const networkAllow = extractYamlList(content, "network_allow") || DEFAULT_POLICY.networkAllow;
11259
+ const envPassthrough = extractYamlList(content, "env_passthrough") || DEFAULT_POLICY.envPassthrough;
11260
+ const timeoutMatch = content.match(/^\s*timeout:\s*(\d+)/m);
11261
+ const memoryMatch = content.match(/^\s*memory:\s*["']?(\S+?)["']?\s*$/m);
11262
+ const cpusMatch = content.match(/^\s*cpus:\s*(\d+)/m);
11263
+ return {
11264
+ networkAllow,
11265
+ envPassthrough,
11266
+ timeout: timeoutMatch ? parseInt(timeoutMatch[1], 10) : DEFAULT_POLICY.timeout,
11267
+ memory: memoryMatch?.[1] || DEFAULT_POLICY.memory,
11268
+ cpus: cpusMatch ? parseInt(cpusMatch[1], 10) : DEFAULT_POLICY.cpus
11269
+ };
11270
+ }
11271
+ function extractYamlList(content, key) {
11272
+ const regex = new RegExp(`^\\s*${key}:\\s*\\n((?:\\s+-\\s*.+\\n?)*)`, "m");
11273
+ const match = content.match(regex);
11274
+ if (!match) return null;
11275
+ return match[1].split("\n").map((line) => {
11276
+ const m = line.match(/^\s*-\s*["']?(.+?)["']?\s*$/);
11277
+ return m ? m[1] : "";
11278
+ }).filter(Boolean);
11279
+ }
11280
+ function buildSandboxEnv(policy) {
11281
+ const env = {
11282
+ PATH: process.env.PATH || "",
11283
+ HOME: process.env.HOME || process.env.USERPROFILE || "",
11284
+ NODE_ENV: "development",
11285
+ FISHI_SANDBOX: "true"
11286
+ };
11287
+ for (const key of policy.envPassthrough) {
11288
+ if (process.env[key]) {
11289
+ env[key] = process.env[key];
11290
+ }
11291
+ }
11292
+ return env;
11293
+ }
11294
+ function runInProcessSandbox(command, args, worktreePath, policy) {
11295
+ const env = buildSandboxEnv(policy);
11296
+ try {
11297
+ const stdout = execFileSync(command, args, {
11298
+ cwd: worktreePath,
11299
+ encoding: "utf-8",
11300
+ timeout: policy.timeout * 1e3,
11301
+ env,
11302
+ stdio: "pipe",
11303
+ maxBuffer: 10 * 1024 * 1024
11304
+ // 10MB
11305
+ });
11306
+ return { stdout, stderr: "", exitCode: 0, timedOut: false };
11307
+ } catch (e) {
11308
+ if (e.killed || e.signal === "SIGTERM") {
11309
+ return { stdout: e.stdout || "", stderr: e.stderr || "", exitCode: 1, timedOut: true };
11310
+ }
11311
+ return {
11312
+ stdout: e.stdout || "",
11313
+ stderr: e.stderr || "",
11314
+ exitCode: e.status ?? 1,
11315
+ timedOut: false
11316
+ };
11317
+ }
11318
+ }
11319
+ function runInDockerSandbox(command, args, worktreePath, policy, options = {}) {
11320
+ const dockerArgs = [
11321
+ "run",
11322
+ "--rm",
11323
+ "--workdir",
11324
+ "/workspace",
11325
+ "-v",
11326
+ `${worktreePath}:/workspace`
11327
+ ];
11328
+ if (options.nodeModulesPath && existsSync5(options.nodeModulesPath)) {
11329
+ dockerArgs.push("-v", `${options.nodeModulesPath}:/workspace/node_modules:ro`);
11330
+ }
11331
+ dockerArgs.push("--memory", policy.memory);
11332
+ dockerArgs.push("--cpus", String(policy.cpus));
11333
+ dockerArgs.push("-e", "FISHI_SANDBOX=true");
11334
+ dockerArgs.push("-e", "NODE_ENV=development");
11335
+ for (const key of policy.envPassthrough) {
11336
+ if (process.env[key]) {
11337
+ dockerArgs.push("-e", `${key}=${process.env[key]}`);
11338
+ }
11339
+ }
11340
+ dockerArgs.push("--network", "host");
11341
+ dockerArgs.push("fishi-sandbox:latest", command, ...args);
11342
+ try {
11343
+ const stdout = execFileSync("docker", dockerArgs, {
11344
+ encoding: "utf-8",
11345
+ timeout: policy.timeout * 1e3,
11346
+ stdio: "pipe",
11347
+ maxBuffer: 10 * 1024 * 1024
11348
+ });
11349
+ return { stdout, stderr: "", exitCode: 0, timedOut: false };
11350
+ } catch (e) {
11351
+ if (e.killed || e.signal === "SIGTERM") {
11352
+ return { stdout: e.stdout || "", stderr: e.stderr || "", exitCode: 1, timedOut: true };
11353
+ }
11354
+ return {
11355
+ stdout: e.stdout || "",
11356
+ stderr: e.stderr || "",
11357
+ exitCode: e.status ?? 1,
11358
+ timedOut: false
11359
+ };
11360
+ }
11361
+ }
11362
+ function runInSandbox(command, args, worktreePath, projectDir, options = {}) {
11363
+ const config = readSandboxConfig(projectDir);
11364
+ const policy = readSandboxPolicy(projectDir);
11365
+ if (config.mode === "docker" && config.dockerAvailable) {
11366
+ return runInDockerSandbox(command, args, worktreePath, policy, options);
11367
+ }
11368
+ return runInProcessSandbox(command, args, worktreePath, policy);
11369
+ }
11370
+
11371
+ // src/templates/configs/sandbox-policy.ts
11372
+ function getSandboxPolicyTemplate() {
11373
+ return `# FISHI Sandbox Policy
11374
+ # Controls what agents can access inside their sandboxed worktrees
11375
+
11376
+ # Network domains agents are allowed to reach
11377
+ network_allow:
11378
+ - registry.npmjs.org
11379
+ - localhost
11380
+ - 127.0.0.1
11381
+
11382
+ # Environment variables passed into the sandbox
11383
+ # Add secrets your agents need (e.g., DATABASE_URL, API_KEY)
11384
+ env_passthrough: []
11385
+
11386
+ # Maximum time (seconds) a single agent command can run
11387
+ timeout: 600
11388
+
11389
+ # Docker resource limits (only applies in docker mode)
11390
+ memory: "2g"
11391
+ cpus: 2
11392
+ `;
11393
+ }
11394
+
11395
+ // src/templates/docker/Dockerfile.ts
11396
+ function getDockerfileTemplate() {
11397
+ return `# FISHI Sandbox Runtime
11398
+ # Minimal Node.js image for agent execution
11399
+ FROM node:22-slim
11400
+
11401
+ # Install git and common build tools
11402
+ RUN apt-get update && apt-get install -y --no-install-recommends \\
11403
+ git \\
11404
+ ca-certificates \\
11405
+ && rm -rf /var/lib/apt/lists/*
11406
+
11407
+ # Set working directory
11408
+ WORKDIR /workspace
11409
+
11410
+ # Non-root user for additional safety
11411
+ RUN groupadd -r fishi && useradd -r -g fishi -m fishi
11412
+ USER fishi
11413
+
11414
+ # Default command
11415
+ CMD ["node"]
11416
+ `;
11417
+ }
11418
+
11419
+ // src/templates/dashboard/index-html.ts
11420
+ function getDashboardHtml() {
11421
+ return `<!DOCTYPE html>
11422
+ <html lang="en">
11423
+ <head>
11424
+ <meta charset="UTF-8" />
11425
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
11426
+ <title>FISHI Agent Dashboard</title>
11427
+ <style>
11428
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
11429
+
11430
+ body {
11431
+ background: #0a0a0f;
11432
+ color: #e2e8f0;
11433
+ font-family: 'Courier New', Courier, monospace;
11434
+ font-size: 14px;
11435
+ min-height: 100vh;
11436
+ }
11437
+
11438
+ /* \u2500\u2500 Header \u2500\u2500 */
11439
+ header {
11440
+ display: flex;
11441
+ align-items: center;
11442
+ justify-content: space-between;
11443
+ padding: 14px 24px;
11444
+ background: #12121a;
11445
+ border-bottom: 1px solid #1e1e2e;
11446
+ }
11447
+ header h1 { font-size: 18px; color: #06b6d4; letter-spacing: 0.05em; }
11448
+ .status-dot {
11449
+ width: 10px; height: 10px;
11450
+ border-radius: 50%;
11451
+ background: #22c55e;
11452
+ display: inline-block;
11453
+ margin-right: 8px;
11454
+ animation: pulse 2s infinite;
11455
+ }
11456
+ .status-dot.offline { background: #ef4444; animation: none; }
11457
+ @keyframes pulse {
11458
+ 0%, 100% { opacity: 1; }
11459
+ 50% { opacity: 0.4; }
11460
+ }
11461
+ .last-updated { color: #6b7280; font-size: 12px; }
11462
+
11463
+ /* \u2500\u2500 Layout \u2500\u2500 */
11464
+ main { padding: 20px 24px; }
11465
+
11466
+ /* \u2500\u2500 Stat cards \u2500\u2500 */
11467
+ .stat-grid {
11468
+ display: grid;
11469
+ grid-template-columns: repeat(4, 1fr);
11470
+ gap: 12px;
11471
+ margin-bottom: 20px;
11472
+ }
11473
+ @media (max-width: 900px) { .stat-grid { grid-template-columns: repeat(2, 1fr); } }
11474
+ .card {
11475
+ background: #12121a;
11476
+ border: 1px solid #1e1e2e;
11477
+ border-radius: 8px;
11478
+ padding: 16px;
11479
+ }
11480
+ .card-label { color: #6b7280; font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 8px; }
11481
+ .card-value { font-size: 28px; color: #06b6d4; font-weight: bold; }
11482
+
11483
+ /* \u2500\u2500 Phase bar \u2500\u2500 */
11484
+ .phase-section { margin-bottom: 20px; }
11485
+ .phase-bar {
11486
+ display: flex;
11487
+ gap: 4px;
11488
+ flex-wrap: wrap;
11489
+ background: #12121a;
11490
+ border: 1px solid #1e1e2e;
11491
+ border-radius: 8px;
11492
+ padding: 12px 16px;
11493
+ }
11494
+ .phase-pill {
11495
+ padding: 4px 12px;
11496
+ border-radius: 4px;
11497
+ font-size: 12px;
11498
+ color: #6b7280;
11499
+ border: 1px solid #2d2d3d;
11500
+ background: #0a0a0f;
11501
+ }
11502
+ .phase-pill.done { color: #22c55e; border-color: #22c55e; background: rgba(34,197,94,0.08); }
11503
+ .phase-pill.active { color: #06b6d4; border-color: #06b6d4; background: rgba(6,182,212,0.12); font-weight: bold; }
11504
+ .phase-arrow { color: #2d2d3d; align-self: center; }
11505
+
11506
+ /* \u2500\u2500 Main grid \u2500\u2500 */
11507
+ .main-grid {
11508
+ display: grid;
11509
+ grid-template-columns: 1fr 1fr;
11510
+ gap: 16px;
11511
+ margin-bottom: 16px;
11512
+ }
11513
+ @media (max-width: 800px) { .main-grid { grid-template-columns: 1fr; } }
11514
+
11515
+ .panel {
11516
+ background: #12121a;
11517
+ border: 1px solid #1e1e2e;
11518
+ border-radius: 8px;
11519
+ padding: 16px;
11520
+ }
11521
+ .panel-title {
11522
+ color: #94a3b8;
11523
+ font-size: 12px;
11524
+ text-transform: uppercase;
11525
+ letter-spacing: 0.1em;
11526
+ margin-bottom: 12px;
11527
+ border-bottom: 1px solid #1e1e2e;
11528
+ padding-bottom: 8px;
11529
+ }
11530
+
11531
+ /* \u2500\u2500 Events \u2500\u2500 */
11532
+ .event-row {
11533
+ display: flex;
11534
+ gap: 10px;
11535
+ padding: 5px 0;
11536
+ border-bottom: 1px solid #1a1a28;
11537
+ font-size: 12px;
11538
+ }
11539
+ .event-row:last-child { border-bottom: none; }
11540
+ .event-ts { color: #4b5563; min-width: 80px; }
11541
+ .event-type { color: #06b6d4; min-width: 130px; }
11542
+ .event-agent { color: #a78bfa; }
11543
+ .empty-msg { color: #4b5563; font-style: italic; }
11544
+
11545
+ /* \u2500\u2500 Agent activity \u2500\u2500 */
11546
+ .agent-row {
11547
+ display: flex;
11548
+ justify-content: space-between;
11549
+ padding: 6px 0;
11550
+ border-bottom: 1px solid #1a1a28;
11551
+ font-size: 12px;
11552
+ }
11553
+ .agent-row:last-child { border-bottom: none; }
11554
+ .agent-name { color: #a78bfa; }
11555
+ .agent-stats { color: #6b7280; display: flex; gap: 12px; }
11556
+ .stat-ok { color: #22c55e; }
11557
+ .stat-fail { color: #ef4444; }
11558
+ .stat-files { color: #06b6d4; }
11559
+
11560
+ /* \u2500\u2500 Bottom row \u2500\u2500 */
11561
+ .bottom-grid {
11562
+ display: grid;
11563
+ grid-template-columns: 1fr 1fr 1fr;
11564
+ gap: 16px;
11565
+ }
11566
+ @media (max-width: 900px) { .bottom-grid { grid-template-columns: 1fr; } }
11567
+
11568
+ /* \u2500\u2500 KV table \u2500\u2500 */
11569
+ .kv-row {
11570
+ display: flex;
11571
+ justify-content: space-between;
11572
+ padding: 4px 0;
11573
+ border-bottom: 1px solid #1a1a28;
11574
+ font-size: 12px;
11575
+ }
11576
+ .kv-row:last-child { border-bottom: none; }
11577
+ .kv-key { color: #fbbf24; }
11578
+ .kv-val { color: #06b6d4; }
11579
+
11580
+ /* \u2500\u2500 Gates \u2500\u2500 */
11581
+ .gate-row {
11582
+ display: flex;
11583
+ align-items: center;
11584
+ gap: 8px;
11585
+ padding: 4px 0;
11586
+ font-size: 12px;
11587
+ border-bottom: 1px solid #1a1a28;
11588
+ }
11589
+ .gate-row:last-child { border-bottom: none; }
11590
+ .gate-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
11591
+ .gate-dot.passed { background: #22c55e; }
11592
+ .gate-dot.failed { background: #ef4444; }
11593
+ .gate-dot.pending { background: #f59e0b; }
11594
+ .gate-name { color: #e2e8f0; flex: 1; }
11595
+ .gate-status { font-size: 11px; }
11596
+ .gate-status.passed { color: #22c55e; }
11597
+ .gate-status.failed { color: #ef4444; }
11598
+ .gate-status.pending { color: #f59e0b; }
11599
+ </style>
11600
+ </head>
11601
+ <body>
11602
+ <header>
11603
+ <h1><span class="status-dot" id="statusDot"></span>FISHI Agent Dashboard</h1>
11604
+ <span class="last-updated" id="lastUpdated">Loading...</span>
11605
+ </header>
11606
+ <main>
11607
+ <!-- Stat cards -->
11608
+ <div class="stat-grid">
11609
+ <div class="card"><div class="card-label">Agent Completions</div><div class="card-value" id="statCompletions">\u2014</div></div>
11610
+ <div class="card"><div class="card-label">Files Changed</div><div class="card-value" id="statFiles">\u2014</div></div>
11611
+ <div class="card"><div class="card-label">Total Tokens</div><div class="card-value" id="statTokens">\u2014</div></div>
11612
+ <div class="card"><div class="card-label">Dynamic Agents</div><div class="card-value" id="statDynamic">\u2014</div></div>
11613
+ </div>
11614
+
11615
+ <!-- Phase bar -->
11616
+ <div class="phase-section">
11617
+ <div class="phase-bar" id="phaseBar">Loading phases...</div>
11618
+ </div>
11619
+
11620
+ <!-- Main grid -->
11621
+ <div class="main-grid">
11622
+ <div class="panel">
11623
+ <div class="panel-title">Recent Events</div>
11624
+ <div id="eventsPanel"><span class="empty-msg">No events yet.</span></div>
11625
+ </div>
11626
+ <div class="panel">
11627
+ <div class="panel-title">Agent Activity</div>
11628
+ <div id="agentsPanel"><span class="empty-msg">No agent activity yet.</span></div>
11629
+ </div>
11630
+ </div>
11631
+
11632
+ <!-- Bottom row -->
11633
+ <div class="bottom-grid">
11634
+ <div class="panel">
11635
+ <div class="panel-title">Tokens by Model</div>
11636
+ <div id="tokensPanel"><span class="empty-msg">No data.</span></div>
11637
+ </div>
11638
+ <div class="panel">
11639
+ <div class="panel-title">Tools Used</div>
11640
+ <div id="toolsPanel"><span class="empty-msg">No data.</span></div>
11641
+ </div>
11642
+ <div class="panel">
11643
+ <div class="panel-title">Gates</div>
11644
+ <div id="gatesPanel"><span class="empty-msg">No gates.</span></div>
11645
+ </div>
11646
+ </div>
11647
+ </main>
11648
+
11649
+ <script>
11650
+ const PHASES = ['init', 'discovery', 'planning', 'development', 'testing', 'review', 'deployed'];
11651
+
11652
+ function fmt(n) {
11653
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
11654
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
11655
+ return String(n);
11656
+ }
11657
+
11658
+ function fmtTime(iso) {
11659
+ try { return new Date(iso).toLocaleTimeString(); } catch { return iso; }
11660
+ }
11661
+
11662
+ function renderPhase(currentPhase) {
11663
+ const idx = PHASES.indexOf(currentPhase);
11664
+ const bar = document.getElementById('phaseBar');
11665
+ let html = '';
11666
+ PHASES.forEach((p, i) => {
11667
+ if (i > 0) html += '<span class="phase-arrow">&#8594;</span>';
11668
+ const cls = i < idx ? 'done' : i === idx ? 'active' : '';
11669
+ html += \`<span class="phase-pill \${cls}">\${p}</span>\`;
11670
+ });
11671
+ bar.innerHTML = html;
11672
+ }
11673
+
11674
+ function renderEvents(events) {
11675
+ const panel = document.getElementById('eventsPanel');
11676
+ const recent = events.slice(-10).reverse();
11677
+ if (!recent.length) { panel.innerHTML = '<span class="empty-msg">No events yet.</span>'; return; }
11678
+ panel.innerHTML = recent.map(ev =>
11679
+ \`<div class="event-row">
11680
+ <span class="event-ts">\${fmtTime(ev.timestamp)}</span>
11681
+ <span class="event-type">\${ev.type}</span>
11682
+ <span class="event-agent">\${ev.agent}</span>
11683
+ </div>\`
11684
+ ).join('');
11685
+ }
11686
+
11687
+ function renderAgents(agentSummary) {
11688
+ const panel = document.getElementById('agentsPanel');
11689
+ const entries = Object.entries(agentSummary);
11690
+ if (!entries.length) { panel.innerHTML = '<span class="empty-msg">No agent activity yet.</span>'; return; }
11691
+ entries.sort((a, b) => b[1].completions - a[1].completions);
11692
+ panel.innerHTML = entries.map(([name, s]) =>
11693
+ \`<div class="agent-row">
11694
+ <span class="agent-name">\${name}</span>
11695
+ <span class="agent-stats">
11696
+ <span class="stat-ok">&#10003; \${s.completions}</span>
11697
+ <span class="stat-fail">\${s.failures > 0 ? '&#10007; ' + s.failures : ''}</span>
11698
+ <span class="stat-files">&#128196; \${s.filesChanged}</span>
11699
+ </span>
11700
+ </div>\`
11701
+ ).join('');
11702
+ }
11703
+
11704
+ function renderKV(data, panelId, keyClass) {
11705
+ const panel = document.getElementById(panelId);
11706
+ const entries = Object.entries(data).sort((a, b) => b[1] - a[1]);
11707
+ if (!entries.length) { panel.innerHTML = '<span class="empty-msg">No data.</span>'; return; }
11708
+ panel.innerHTML = entries.map(([k, v]) =>
11709
+ \`<div class="kv-row"><span class="kv-key">\${k}</span><span class="kv-val">\${fmt(v)}</span></div>\`
11710
+ ).join('');
11711
+ }
11712
+
11713
+ function renderGates(gates) {
11714
+ const panel = document.getElementById('gatesPanel');
11715
+ if (!gates || !gates.length) { panel.innerHTML = '<span class="empty-msg">No gates.</span>'; return; }
11716
+ panel.innerHTML = gates.map(g => {
11717
+ const cls = g.status === 'passed' ? 'passed' : g.status === 'failed' ? 'failed' : 'pending';
11718
+ return \`<div class="gate-row">
11719
+ <span class="gate-dot \${cls}"></span>
11720
+ <span class="gate-name">\${g.name}</span>
11721
+ <span class="gate-status \${cls}">\${g.status}</span>
11722
+ </div>\`;
11723
+ }).join('');
11724
+ }
11725
+
11726
+ async function fetchAndRender() {
11727
+ const dot = document.getElementById('statusDot');
11728
+ const lu = document.getElementById('lastUpdated');
11729
+ try {
11730
+ const res = await fetch('/api/state');
11731
+ if (!res.ok) throw new Error('HTTP ' + res.status);
11732
+ const data = await res.json();
11733
+
11734
+ dot.classList.remove('offline');
11735
+
11736
+ const s = data.summary || {};
11737
+ document.getElementById('statCompletions').textContent = fmt(s.totalAgentCompletions || 0);
11738
+ document.getElementById('statFiles').textContent = fmt(s.totalFilesChanged || 0);
11739
+ document.getElementById('statTokens').textContent = fmt(s.totalTokens || 0);
11740
+ document.getElementById('statDynamic').textContent = fmt(s.dynamicAgentsCreated || 0);
11741
+
11742
+ renderPhase(data.phase || 'init');
11743
+ renderEvents(data.events || []);
11744
+ renderAgents(data.agentSummary || {});
11745
+ renderKV(s.tokensByModel || {}, 'tokensPanel');
11746
+ renderKV(s.toolsUsed || {}, 'toolsPanel');
11747
+ renderGates(data.gates || []);
11748
+
11749
+ lu.textContent = 'Updated: ' + new Date().toLocaleTimeString();
11750
+ } catch (err) {
11751
+ dot.classList.add('offline');
11752
+ lu.textContent = 'Error: ' + err.message;
11753
+ }
11754
+ }
11755
+
11756
+ fetchAndRender();
11757
+ setInterval(fetchAndRender, 2000);
11758
+ </script>
11759
+ </body>
11760
+ </html>`;
11761
+ }
10942
11762
  export {
10943
11763
  architectAgentTemplate,
10944
11764
  backendAgentTemplate,
11765
+ buildSandboxEnv,
10945
11766
  createBackup,
10946
11767
  detectConflicts,
11768
+ detectDocker,
10947
11769
  devLeadTemplate,
10948
11770
  devopsAgentTemplate,
10949
11771
  docsAgentTemplate,
11772
+ emitEvent,
10950
11773
  frontendAgentTemplate,
10951
11774
  fullstackAgentTemplate,
10952
11775
  generateScaffold,
@@ -10954,6 +11777,7 @@ export {
10954
11777
  getAgentCompleteHook,
10955
11778
  getAgentFactoryTemplate,
10956
11779
  getAgentRegistryTemplate,
11780
+ getAgentSummary,
10957
11781
  getApiDesignSkill,
10958
11782
  getAutoCheckpointHook,
10959
11783
  getBoardCommand,
@@ -10963,9 +11787,11 @@ export {
10963
11787
  getClaudeMdTemplate,
10964
11788
  getCodeGenSkill,
10965
11789
  getCoordinatorFactoryTemplate,
11790
+ getDashboardHtml,
10966
11791
  getDebuggingSkill,
10967
11792
  getDeploymentSkill,
10968
11793
  getDocCheckerScript,
11794
+ getDockerfileTemplate,
10969
11795
  getDocumentationSkill,
10970
11796
  getFishiYamlTemplate,
10971
11797
  getGateCommand,
@@ -10977,6 +11803,7 @@ export {
10977
11803
  getMcpJsonTemplate,
10978
11804
  getMemoryManagerScript,
10979
11805
  getModelRoutingReference,
11806
+ getMonitorEmitterScript,
10980
11807
  getPhaseRunnerScript,
10981
11808
  getPostEditHook,
10982
11809
  getPrdCommand,
@@ -10985,6 +11812,7 @@ export {
10985
11812
  getResetCommand,
10986
11813
  getResumeCommand,
10987
11814
  getSafetyCheckHook,
11815
+ getSandboxPolicyTemplate,
10988
11816
  getSessionStartHook,
10989
11817
  getSettingsJsonTemplate,
10990
11818
  getSprintCommand,
@@ -11006,7 +11834,13 @@ export {
11006
11834
  planningAgentTemplate,
11007
11835
  planningLeadTemplate,
11008
11836
  qualityLeadTemplate,
11837
+ readMonitorState,
11838
+ readSandboxConfig,
11839
+ readSandboxPolicy,
11009
11840
  researchAgentTemplate,
11841
+ runInDockerSandbox,
11842
+ runInProcessSandbox,
11843
+ runInSandbox,
11010
11844
  securityAgentTemplate,
11011
11845
  testingAgentTemplate,
11012
11846
  uiuxAgentTemplate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qlucent/fishi-core",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Shared templates, types, and generators for the FISHI framework",
5
5
  "license": "MIT",
6
6
  "type": "module",