@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 +104 -1
- package/dist/index.js +836 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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.
|
|
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">→</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">✓ \${s.completions}</span>
|
|
11697
|
+
<span class="stat-fail">\${s.failures > 0 ? '✗ ' + s.failures : ''}</span>
|
|
11698
|
+
<span class="stat-files">📄 \${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,
|