agent-enderun 1.10.4 → 1.11.1
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/README.md +34 -45
- package/dist/framework-mcp/tests/tools/quality/check_lint.test.js +1 -1
- package/dist/framework-mcp/tests/tools/quality/check_lint.test.js.map +1 -1
- package/dist/src/cli/adapters/core.d.ts +1 -1
- package/dist/src/cli/adapters/core.js +0 -1
- package/dist/src/cli/adapters/core.js.map +1 -1
- package/dist/src/cli/adapters/index.d.ts +1 -0
- package/dist/src/cli/adapters/index.js +1 -0
- package/dist/src/cli/adapters/index.js.map +1 -1
- package/dist/src/cli/adapters/scaffold.d.ts +1 -1
- package/dist/src/cli/adapters/scaffold.js +13 -7
- package/dist/src/cli/adapters/scaffold.js.map +1 -1
- package/dist/src/cli/commands/init/scaffold-core.d.ts +5 -2
- package/dist/src/cli/commands/init/scaffold-core.js +28 -6
- package/dist/src/cli/commands/init/scaffold-core.js.map +1 -1
- package/dist/src/cli/commands/init.js +31 -4
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/orchestrate.d.ts +9 -7
- package/dist/src/cli/commands/orchestrate.js +40 -4
- package/dist/src/cli/commands/orchestrate.js.map +1 -1
- package/dist/src/cli/commands/plan.d.ts +3 -1
- package/dist/src/cli/commands/plan.js +79 -22
- package/dist/src/cli/commands/plan.js.map +1 -1
- package/dist/src/cli/index.js +11 -15
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/cli/utils/compliance.js +0 -1
- package/dist/src/cli/utils/compliance.js.map +1 -1
- package/dist/src/cli/utils/config-schema.d.ts +7 -7
- package/dist/src/cli/utils/pkg.d.ts +19 -0
- package/dist/src/cli/utils/pkg.js +29 -5
- package/dist/src/cli/utils/pkg.js.map +1 -1
- package/dist/src/modules/adapters/definitions.js +1 -1
- package/dist/src/modules/adapters/definitions.js.map +1 -1
- package/dist/src/modules/adapters/shared.js +7 -4
- package/dist/src/modules/adapters/shared.js.map +1 -1
- package/dist/src/modules/agents/definitions.d.ts +1 -1
- package/dist/src/modules/agents/definitions.js +129 -69
- package/dist/src/modules/agents/definitions.js.map +1 -1
- package/dist/src/modules/agents/registry/analyst.js +5 -1
- package/dist/src/modules/agents/registry/analyst.js.map +1 -1
- package/dist/src/modules/agents/registry/backend.js +5 -4
- package/dist/src/modules/agents/registry/backend.js.map +1 -1
- package/dist/src/modules/agents/registry/database.js +2 -1
- package/dist/src/modules/agents/registry/database.js.map +1 -1
- package/dist/src/modules/agents/registry/devops.js +2 -0
- package/dist/src/modules/agents/registry/devops.js.map +1 -1
- package/dist/src/modules/agents/registry/explorer.js +1 -0
- package/dist/src/modules/agents/registry/explorer.js.map +1 -1
- package/dist/src/modules/agents/registry/git.js +3 -1
- package/dist/src/modules/agents/registry/git.js.map +1 -1
- package/dist/src/modules/agents/registry/manager.js +8 -2
- package/dist/src/modules/agents/registry/manager.js.map +1 -1
- package/dist/src/modules/agents/registry/native.js +5 -1
- package/dist/src/modules/agents/registry/native.js.map +1 -1
- package/dist/src/modules/agents/registry/security.js +1 -0
- package/dist/src/modules/agents/registry/security.js.map +1 -1
- package/dist/src/shared/constants.d.ts +1 -1
- package/dist/src/shared/constants.js +10 -9
- package/dist/src/shared/constants.js.map +1 -1
- package/dist/tests/approve.test.js +4 -9
- package/dist/tests/approve.test.js.map +1 -1
- package/dist/tests/integration/agent_flow.test.js +2 -2
- package/dist/tests/integration/agent_flow.test.js.map +1 -1
- package/dist/tests/orchestrate.test.js +2 -7
- package/dist/tests/orchestrate.test.js.map +1 -1
- package/framework-mcp/dist/constants.js +64 -0
- package/framework-mcp/dist/index.js +109 -0
- package/framework-mcp/dist/tools/control_plane/locking.js +64 -0
- package/framework-mcp/dist/tools/control_plane/registry.js +34 -0
- package/framework-mcp/dist/tools/dashboard/start_dashboard.js +29 -0
- package/framework-mcp/dist/tools/definitions.js +300 -0
- package/framework-mcp/dist/tools/file_system/batch_surgical_edit.js +59 -0
- package/framework-mcp/dist/tools/file_system/patch_file.js +25 -0
- package/framework-mcp/dist/tools/file_system/read_file.js +51 -0
- package/framework-mcp/dist/tools/file_system/replace_text.js +43 -0
- package/framework-mcp/dist/tools/file_system/write_file.js +38 -0
- package/framework-mcp/dist/tools/framework/audit_deps.js +41 -0
- package/framework-mcp/dist/tools/framework/get_status.js +5 -0
- package/framework-mcp/dist/tools/framework/orchestrate.js +5 -0
- package/framework-mcp/dist/tools/framework/run_tests.js +25 -0
- package/framework-mcp/dist/tools/framework/update_contract_hash.js +5 -0
- package/framework-mcp/dist/tools/framework/update_memory.js +8 -0
- package/framework-mcp/dist/tools/index.js +62 -0
- package/framework-mcp/dist/tools/memory/get_insights.js +34 -0
- package/framework-mcp/dist/tools/memory/read_memory.js +28 -0
- package/framework-mcp/dist/tools/messaging/log_action.js +22 -0
- package/framework-mcp/dist/tools/messaging/send_message.js +87 -0
- package/framework-mcp/dist/tools/observability/check_ports.js +26 -0
- package/framework-mcp/dist/tools/observability/get_health.js +19 -0
- package/framework-mcp/dist/tools/quality/check_lint.js +28 -0
- package/framework-mcp/dist/tools/search/get_gaps.js +48 -0
- package/framework-mcp/dist/tools/search/get_map.js +43 -0
- package/framework-mcp/dist/tools/search/grep_search.js +76 -0
- package/framework-mcp/dist/tools/search/list_dir.js +28 -0
- package/framework-mcp/dist/tools/shell/run_command.js +46 -0
- package/framework-mcp/dist/tools/types.js +1 -0
- package/framework-mcp/dist/utils/cli.js +20 -0
- package/framework-mcp/dist/utils/compliance.js +29 -0
- package/framework-mcp/dist/utils/fs.js +44 -0
- package/framework-mcp/dist/utils/metrics.js +56 -0
- package/framework-mcp/dist/utils/security.js +60 -0
- package/framework-mcp/package.json +19 -0
- package/framework-mcp/src/constants.ts +78 -0
- package/framework-mcp/src/declarations.d.ts +17 -0
- package/framework-mcp/src/index.ts +132 -0
- package/framework-mcp/src/tools/control_plane/locking.ts +71 -0
- package/framework-mcp/src/tools/control_plane/registry.ts +38 -0
- package/framework-mcp/src/tools/dashboard/start_dashboard.ts +33 -0
- package/framework-mcp/src/tools/definitions.ts +302 -0
- package/framework-mcp/src/tools/file_system/batch_surgical_edit.ts +79 -0
- package/framework-mcp/src/tools/file_system/patch_file.ts +33 -0
- package/framework-mcp/src/tools/file_system/read_file.ts +58 -0
- package/framework-mcp/src/tools/file_system/replace_text.ts +52 -0
- package/framework-mcp/src/tools/file_system/write_file.ts +45 -0
- package/framework-mcp/src/tools/framework/audit_deps.ts +49 -0
- package/framework-mcp/src/tools/framework/get_status.ts +7 -0
- package/framework-mcp/src/tools/framework/orchestrate.ts +7 -0
- package/framework-mcp/src/tools/framework/run_tests.ts +28 -0
- package/framework-mcp/src/tools/framework/update_contract_hash.ts +7 -0
- package/framework-mcp/src/tools/framework/update_memory.ts +10 -0
- package/framework-mcp/src/tools/index.ts +66 -0
- package/framework-mcp/src/tools/memory/get_insights.ts +41 -0
- package/framework-mcp/src/tools/memory/read_memory.ts +31 -0
- package/framework-mcp/src/tools/messaging/log_action.ts +28 -0
- package/framework-mcp/src/tools/messaging/send_message.ts +89 -0
- package/framework-mcp/src/tools/observability/check_ports.ts +30 -0
- package/framework-mcp/src/tools/observability/get_health.ts +24 -0
- package/framework-mcp/src/tools/quality/check_lint.ts +33 -0
- package/framework-mcp/src/tools/search/get_gaps.ts +54 -0
- package/framework-mcp/src/tools/search/get_map.ts +48 -0
- package/framework-mcp/src/tools/search/grep_search.ts +76 -0
- package/framework-mcp/src/tools/search/list_dir.ts +34 -0
- package/framework-mcp/src/tools/shell/run_command.ts +56 -0
- package/framework-mcp/src/tools/types.ts +89 -0
- package/framework-mcp/src/utils/cli.ts +20 -0
- package/framework-mcp/src/utils/compliance.ts +37 -0
- package/framework-mcp/src/utils/fs.ts +45 -0
- package/framework-mcp/src/utils/metrics.ts +73 -0
- package/framework-mcp/src/utils/security.ts +66 -0
- package/framework-mcp/tests/tools/file_system/file_system_tools.test.ts +212 -0
- package/framework-mcp/tests/tools/messaging/send_message.test.ts +136 -0
- package/framework-mcp/tests/tools/quality/check_lint.test.ts +46 -0
- package/framework-mcp/tests/tools/shell/run_command.test.ts +55 -0
- package/framework-mcp/tsconfig.json +14 -0
- package/package.json +5 -3
- package/src/cli/adapters/core.ts +2 -3
- package/src/cli/adapters/index.ts +1 -0
- package/src/cli/adapters/scaffold.ts +20 -7
- package/src/cli/commands/init/scaffold-core.ts +45 -6
- package/src/cli/commands/init.ts +31 -2
- package/src/cli/commands/orchestrate.ts +41 -4
- package/src/cli/commands/plan.ts +89 -23
- package/src/cli/index.ts +14 -19
- package/src/cli/utils/compliance.ts +8 -9
- package/src/cli/utils/pkg.ts +42 -13
- package/src/modules/adapters/definitions.ts +1 -1
- package/src/modules/adapters/shared.ts +7 -4
- package/src/modules/agents/definitions.ts +140 -73
- package/src/modules/agents/registry/analyst.ts +5 -1
- package/src/modules/agents/registry/backend.ts +5 -4
- package/src/modules/agents/registry/database.ts +2 -1
- package/src/modules/agents/registry/devops.ts +2 -0
- package/src/modules/agents/registry/explorer.ts +1 -0
- package/src/modules/agents/registry/git.ts +3 -1
- package/src/modules/agents/registry/manager.ts +8 -2
- package/src/modules/agents/registry/native.ts +5 -1
- package/src/modules/agents/registry/security.ts +1 -0
- package/src/shared/constants.ts +14 -13
- package/templates/prompts/bug-fix-recipe.md +20 -0
- package/templates/prompts/contract-design-recipe.md +21 -0
- package/templates/prompts/db-management-recipe.md +25 -0
- package/templates/prompts/deployment-recipe.md +23 -0
- package/templates/prompts/new-feature-recipe.md +19 -0
- package/templates/prompts/performance-optimization-recipe.md +23 -0
- package/templates/prompts/pull-request-template.md +21 -0
- package/templates/prompts/refactoring-recipe.md +21 -0
- package/templates/prompts/security-audit-recipe.md +20 -0
- package/templates/standards/architecture-standards.md +23 -0
- package/templates/standards/auth-standards.md +125 -0
- package/templates/standards/crud-governance.md +21 -0
- package/templates/standards/deployment-standards.md +21 -0
- package/templates/standards/frontend-standards.md +37 -0
- package/templates/standards/github-actions-standards.md +43 -0
- package/templates/standards/i18n-standards.md +17 -0
- package/templates/standards/kysely-standards.md +47 -0
- package/templates/standards/llm-governance.md +15 -0
- package/templates/standards/logging-and-secrets.md +34 -0
- package/templates/standards/mobile-standards.md +23 -0
- package/templates/standards/observability-standards.md +15 -0
- package/templates/standards/performance-standards.md +15 -0
- package/templates/standards/pino-standards.md +46 -0
- package/templates/standards/playwright-standards.md +54 -0
- package/templates/standards/quality-standards.md +31 -0
- package/templates/standards/react-query-standards.md +72 -0
- package/templates/standards/react-router-standards.md +62 -0
- package/templates/standards/security-audit-standards.md +16 -0
- package/templates/standards/security-standards.md +21 -0
- package/templates/standards/swagger-standards.md +50 -0
- package/templates/standards/tailwind-standards.md +20 -0
- package/templates/standards/testing-standards.md +31 -0
- package/templates/standards/typeorm-standards.md +49 -0
- package/templates/standards/vitest-standards.md +110 -0
- package/src/cli/commands/app.ts +0 -56
- package/src/cli/utils/app-backend.ts +0 -257
- package/src/cli/utils/app-docs.ts +0 -83
- package/src/cli/utils/app-frontend.ts +0 -263
- package/src/cli/utils/app-inferrer.ts +0 -63
- package/src/cli/utils/app-mobile.ts +0 -113
- package/src/cli/utils/app-types.ts +0 -248
- package/src/cli/utils/app.ts +0 -6
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export interface ToolDefinition {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: "object";
|
|
6
|
+
properties: Record<string, unknown>;
|
|
7
|
+
required?: string[];
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ─── File System ────────────────────────────────────────────────
|
|
12
|
+
export interface ReadFileArgs { path: string; startLine?: number; endLine?: number; }
|
|
13
|
+
export interface WriteFileArgs { path: string; content: string; }
|
|
14
|
+
export interface ReplaceTextArgs { path: string; oldText: string; newText: string; allowMultiple?: boolean; }
|
|
15
|
+
export interface PatchFileArgs { path: string; startLine: number; endLine: number; newContent: string; }
|
|
16
|
+
export interface BatchSurgicalEditArgs {
|
|
17
|
+
edits: Array<{ path: string; oldText: string; newText: string; allowMultiple?: boolean; }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── Search & Discovery ──────────────────────────────────────────
|
|
21
|
+
export interface ListDirArgs { path?: string; }
|
|
22
|
+
export interface GrepSearchArgs { pattern: string; includePattern?: string; excludePattern?: string; }
|
|
23
|
+
export interface GetProjectMapArgs { maxDepth?: number; includeFiles?: boolean; }
|
|
24
|
+
export interface GetProjectGapsArgs { path?: string; }
|
|
25
|
+
|
|
26
|
+
// ─── Messaging (Hermes) ─────────────────────────────────────────
|
|
27
|
+
export interface SendAgentMessageArgs {
|
|
28
|
+
from: string;
|
|
29
|
+
to: string;
|
|
30
|
+
category: "ACTION" | "DELEGATION" | "SUBTASK" | "REPLY" | "ALERT";
|
|
31
|
+
content: string;
|
|
32
|
+
traceId: string;
|
|
33
|
+
parentId?: string;
|
|
34
|
+
priority?: "HIGH" | "NORMAL" | "LOW";
|
|
35
|
+
requiresApproval?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface LogAgentActionArgs {
|
|
38
|
+
agent: string;
|
|
39
|
+
action: string;
|
|
40
|
+
traceId: string;
|
|
41
|
+
status: "SUCCESS" | "FAILURE";
|
|
42
|
+
summary: string;
|
|
43
|
+
findings?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Control Plane ──────────────────────────────────────────────
|
|
47
|
+
export interface AcquireLockArgs { resource: string; agent: string; ttl?: number; }
|
|
48
|
+
export interface ReleaseLockArgs { resource: string; agent: string; }
|
|
49
|
+
export interface RegisterAgentArgs { agent: string; role: string; capability?: number; }
|
|
50
|
+
|
|
51
|
+
// ─── Observability & Utils ──────────────────────────────────────
|
|
52
|
+
export interface StartDashboardArgs { port?: number; }
|
|
53
|
+
export interface CheckActivePortsArgs { filter?: string; }
|
|
54
|
+
export interface RunTestsArgs { command?: string; timeout?: number; }
|
|
55
|
+
export interface UpdateProjectMemoryArgs { section: string; content: string; }
|
|
56
|
+
export interface GetStatusArgs { timeout?: number; }
|
|
57
|
+
export interface OrchestrateArgs { timeout?: number; }
|
|
58
|
+
export interface UpdateContractHashArgs { timeout?: number; }
|
|
59
|
+
export interface RunCommandArgs { command: string; }
|
|
60
|
+
|
|
61
|
+
export type ToolArgs =
|
|
62
|
+
| ReadFileArgs
|
|
63
|
+
| WriteFileArgs
|
|
64
|
+
| ReplaceTextArgs
|
|
65
|
+
| PatchFileArgs
|
|
66
|
+
| BatchSurgicalEditArgs
|
|
67
|
+
| ListDirArgs
|
|
68
|
+
| GrepSearchArgs
|
|
69
|
+
| GetProjectMapArgs
|
|
70
|
+
| GetProjectGapsArgs
|
|
71
|
+
| SendAgentMessageArgs
|
|
72
|
+
| LogAgentActionArgs
|
|
73
|
+
| AcquireLockArgs
|
|
74
|
+
| ReleaseLockArgs
|
|
75
|
+
| RegisterAgentArgs
|
|
76
|
+
| StartDashboardArgs
|
|
77
|
+
| CheckActivePortsArgs
|
|
78
|
+
| RunTestsArgs
|
|
79
|
+
| UpdateProjectMemoryArgs
|
|
80
|
+
| GetStatusArgs
|
|
81
|
+
| OrchestrateArgs
|
|
82
|
+
| UpdateContractHashArgs;
|
|
83
|
+
|
|
84
|
+
export interface ToolResult {
|
|
85
|
+
isError?: boolean;
|
|
86
|
+
content: Array<{ type: "text"; text: string }>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type ToolHandler = (projectRoot: string, args: unknown) => ToolResult | Promise<ToolResult>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Executes a CLI command safely using execFileSync (no shell injection risk).
|
|
5
|
+
* Captures stderr gracefully to prevent stream crashes.
|
|
6
|
+
*/
|
|
7
|
+
export function safeExec(command: string, args: string[], cwd: string, timeout?: number): string {
|
|
8
|
+
try {
|
|
9
|
+
return execFileSync(command, args, {
|
|
10
|
+
cwd,
|
|
11
|
+
encoding: "utf8",
|
|
12
|
+
timeout: timeout || 30000,
|
|
13
|
+
maxBuffer: 1024 * 1024,
|
|
14
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
15
|
+
});
|
|
16
|
+
} catch (error: unknown) {
|
|
17
|
+
const err = error as { stderr?: string; message?: string };
|
|
18
|
+
throw new Error(err.stderr || err.message || "Command execution failed", { cause: error });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise Compliance Guardrail
|
|
3
|
+
* Checks content against corporate standards before allowing file mutations.
|
|
4
|
+
*/
|
|
5
|
+
export function verifyCorporateCompliance(content: string, filePath: string): void {
|
|
6
|
+
// 1. Zero Console Policy
|
|
7
|
+
if (content.includes("console.log(") || content.includes("console.warn(") || content.includes("console.error(")) {
|
|
8
|
+
if (!filePath.includes("logger.ts") && !filePath.includes("check.ts") && !filePath.includes("cli.ts")) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"❌ Corporate Compliance Breach: 'console.log/warn/error' usage is forbidden. " +
|
|
11
|
+
"Use the 'logger' system from '@/shared/logger' instead."
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 2. No Explicit Any Policy
|
|
17
|
+
const anyRegex = /[:<]\s*any\b|\bas\s+any\b/g;
|
|
18
|
+
if (anyRegex.test(content)) {
|
|
19
|
+
if (!filePath.includes("definitions.ts") && !filePath.includes("types.ts")) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"❌ Corporate Compliance Breach: 'any' type is forbidden. " +
|
|
22
|
+
"Use 'unknown', 'generics', or proper interfaces for type safety."
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 3. Hardcoded Secrets Guard (Simple heuristic)
|
|
28
|
+
const secretKeywords = [/API_KEY\s*=\s*['"][^'"]+['"]/i, /SECRET\s*=\s*['"][^'"]+['"]/i, /PASSWORD\s*=\s*['"][^'"]+['"]/i];
|
|
29
|
+
for (const regex of secretKeywords) {
|
|
30
|
+
if (regex.test(content) && !filePath.endsWith(".env.example")) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
"❌ Corporate Compliance Breach: Hardcoded secrets detected. " +
|
|
33
|
+
"All sensitive data must be managed via '.env' files."
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Ensures directory existence.
|
|
6
|
+
*/
|
|
7
|
+
export function ensureDir(dirPath: string): void {
|
|
8
|
+
if (!fs.existsSync(dirPath)) {
|
|
9
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Atomically writes a text file.
|
|
15
|
+
*/
|
|
16
|
+
export function writeTextFileAtomic(filePath: string, content: string): void {
|
|
17
|
+
const dir = path.dirname(filePath);
|
|
18
|
+
ensureDir(dir);
|
|
19
|
+
|
|
20
|
+
const tempPath = `${filePath}.${Math.random().toString(36).slice(2, 9)}.tmp`;
|
|
21
|
+
const finalContent = content.endsWith("\n") ? content : `${content}\n`;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
fs.writeFileSync(tempPath, finalContent, "utf8");
|
|
25
|
+
fs.renameSync(tempPath, filePath);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (fs.existsSync(tempPath)) {
|
|
28
|
+
try { fs.unlinkSync(tempPath); } catch { /* ignore */ }
|
|
29
|
+
}
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Atomically appends to a file (if supported by OS) or simulates it.
|
|
36
|
+
* Note: Real atomic append on POSIX is a single write() call with O_APPEND.
|
|
37
|
+
* For simplicity and robustness across platforms, we use a simple append here
|
|
38
|
+
* as the risk of corruption is lower than a full rewrite, but for logs
|
|
39
|
+
* it's acceptable.
|
|
40
|
+
*/
|
|
41
|
+
export function appendFileSafe(filePath: string, content: string): void {
|
|
42
|
+
const dir = path.dirname(filePath);
|
|
43
|
+
ensureDir(dir);
|
|
44
|
+
fs.appendFileSync(filePath, content, "utf8");
|
|
45
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { resolveFrameworkDir } from "./security.js";
|
|
4
|
+
/**
|
|
5
|
+
* Token and Metric Tracker for Agent Enderun.
|
|
6
|
+
* Estimates token usage and logs operational costs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface MetricEntry {
|
|
10
|
+
timestamp: string;
|
|
11
|
+
agent: string;
|
|
12
|
+
action: string;
|
|
13
|
+
estimatedTokens: number;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Metrics = {
|
|
18
|
+
/**
|
|
19
|
+
* Estimates tokens based on character count (rough heuristic: 1 token ~= 4 chars).
|
|
20
|
+
*/
|
|
21
|
+
estimateTokens: (text: string): number => {
|
|
22
|
+
return Math.ceil(text.length / 4);
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Logs the token usage and action to the observability metrics file.
|
|
27
|
+
*/
|
|
28
|
+
logUsage: (projectRoot: string, agent: string, action: string, tokens: number) => {
|
|
29
|
+
Metrics.saveMetric(projectRoot, {
|
|
30
|
+
timestamp: new Date().toISOString(),
|
|
31
|
+
agent,
|
|
32
|
+
action,
|
|
33
|
+
estimatedTokens: tokens
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Logs an error occurrence to the observability metrics file.
|
|
39
|
+
*/
|
|
40
|
+
logError: (projectRoot: string, agent: string, action: string, error: string) => {
|
|
41
|
+
Metrics.saveMetric(projectRoot, {
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
agent,
|
|
44
|
+
action: `ERROR: ${action}`,
|
|
45
|
+
estimatedTokens: 0,
|
|
46
|
+
error
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Internal helper to save metric entries.
|
|
52
|
+
*/
|
|
53
|
+
saveMetric: (projectRoot: string, entry: MetricEntry) => {
|
|
54
|
+
const frameworkDir = resolveFrameworkDir(projectRoot);
|
|
55
|
+
const metricsPath = path.join(projectRoot, frameworkDir, "observability/metrics.json");
|
|
56
|
+
try {
|
|
57
|
+
const metricsDir = path.dirname(metricsPath);
|
|
58
|
+
if (!fs.existsSync(metricsDir)) fs.mkdirSync(metricsDir, { recursive: true });
|
|
59
|
+
|
|
60
|
+
let currentMetrics: MetricEntry[] = [];
|
|
61
|
+
if (fs.existsSync(metricsPath)) {
|
|
62
|
+
currentMetrics = JSON.parse(fs.readFileSync(metricsPath, "utf8"));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
currentMetrics.push(entry);
|
|
66
|
+
|
|
67
|
+
// Keep only last 100 entries to save space
|
|
68
|
+
if (currentMetrics.length > 100) currentMetrics.shift();
|
|
69
|
+
|
|
70
|
+
fs.writeFileSync(metricsPath, JSON.stringify(currentMetrics, null, 2));
|
|
71
|
+
} catch { /* ignore: metrics should not block the main process */ }
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { FRAMEWORK, MCP, UNIFIED_HUB_DIR } from "../constants.js"; // New import
|
|
4
|
+
import os from "os"; // Need os.homedir()
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validates and resolves a user-provided path to prevent path traversal attacks.
|
|
8
|
+
* Ensures the resolved path stays within the project root boundary.
|
|
9
|
+
*/
|
|
10
|
+
export function safePath(projectRoot: string, userPath: string): string {
|
|
11
|
+
const resolved = path.resolve(projectRoot, userPath);
|
|
12
|
+
const normalizedRoot = path.resolve(projectRoot);
|
|
13
|
+
|
|
14
|
+
if (!resolved.startsWith(normalizedRoot + path.sep) && resolved !== normalizedRoot) {
|
|
15
|
+
throw new Error(`Access denied: path "${userPath}" escapes project root.`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolves the active framework directory.
|
|
23
|
+
* Priority: ENDERUN_TEST_DIR (env) -> package.json `enderun.frameworkDir` -> `.enderun` -> other adapter dirs -> global HOME.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveFrameworkDir(projectRoot: string): string {
|
|
26
|
+
// For test environments, use the explicitly set test directory.
|
|
27
|
+
const testDir = process.env[MCP.TEST_DIR_ENV];
|
|
28
|
+
if (testDir) return testDir;
|
|
29
|
+
|
|
30
|
+
// 1. Authoritative source: read from package.json if present
|
|
31
|
+
try {
|
|
32
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
33
|
+
if (fs.existsSync(pkgPath)) {
|
|
34
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
|
|
35
|
+
const enderunConfig = pkg["enderun"] as Record<string, unknown> | undefined;
|
|
36
|
+
if (enderunConfig && typeof enderunConfig["frameworkDir"] === "string") {
|
|
37
|
+
// Ensure the path is relative if it's within the project, otherwise use as-is.
|
|
38
|
+
const resolvedDir = path.resolve(projectRoot, enderunConfig["frameworkDir"]);
|
|
39
|
+
if (resolvedDir.startsWith(path.resolve(projectRoot))) {
|
|
40
|
+
return path.relative(projectRoot, resolvedDir);
|
|
41
|
+
}
|
|
42
|
+
return enderunConfig["frameworkDir"];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// ignore — fall through to filesystem scan
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. Filesystem scan in projectRoot for common framework directories
|
|
50
|
+
const localCandidates = [
|
|
51
|
+
FRAMEWORK.CORE_DIR, // .enderun
|
|
52
|
+
UNIFIED_HUB_DIR, // .agents
|
|
53
|
+
// Add other adapter specific directories if needed, or remove if unified is strictly enforced
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
for (const candidate of localCandidates) {
|
|
57
|
+
const candidatePath = path.join(projectRoot, candidate);
|
|
58
|
+
if (fs.existsSync(candidatePath)) {
|
|
59
|
+
return candidate;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 3. Fallback to global home directory.
|
|
64
|
+
const homeDir = os.homedir();
|
|
65
|
+
return path.join(homeDir, FRAMEWORK.CORE_DIR);
|
|
66
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { handleReplaceText } from "../../../src/tools/file_system/replace_text.js";
|
|
5
|
+
import { handlePatchFile } from "../../../src/tools/file_system/patch_file.js";
|
|
6
|
+
import { handleReadFile } from "../../../src/tools/file_system/read_file.js";
|
|
7
|
+
import { handleWriteFile } from "../../../src/tools/file_system/write_file.js";
|
|
8
|
+
import { ToolArgs } from "../../../src/tools/types.js";
|
|
9
|
+
|
|
10
|
+
import os from "os"; // Need to import os
|
|
11
|
+
|
|
12
|
+
let TEST_DIR: string; // Declare as let
|
|
13
|
+
let TEST_FILE: string;
|
|
14
|
+
let MEMORY_DIR: string;
|
|
15
|
+
let MEMORY_FILE: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
TEST_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "fs-tools-test-")); // Unique temp dir
|
|
19
|
+
fs.mkdirSync(path.join(TEST_DIR, ".enderun"));
|
|
20
|
+
TEST_FILE = path.join(TEST_DIR, "test_file.txt");
|
|
21
|
+
MEMORY_DIR = path.join(TEST_DIR, ".enderun/memory");
|
|
22
|
+
MEMORY_FILE = path.join(MEMORY_DIR, "PROJECT_MEMORY.md");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("File System Tools", () => {
|
|
30
|
+
|
|
31
|
+
describe("handleReplaceText", () => {
|
|
32
|
+
it("should replace a single occurrence of text", () => {
|
|
33
|
+
fs.writeFileSync(TEST_FILE, "hello world", "utf8");
|
|
34
|
+
const args: ToolArgs = {
|
|
35
|
+
path: TEST_FILE,
|
|
36
|
+
oldText: "world",
|
|
37
|
+
newText: "there",
|
|
38
|
+
};
|
|
39
|
+
const result = handleReplaceText(TEST_DIR, args as any);
|
|
40
|
+
expect(result.content[0].text).toContain("✅ Surgical edit successful in");
|
|
41
|
+
expect(fs.readFileSync(TEST_FILE, "utf8")).toBe("hello there\n");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should throw an error if text is not found", () => {
|
|
45
|
+
fs.writeFileSync(TEST_FILE, "hello world", "utf8");
|
|
46
|
+
const args: ToolArgs = {
|
|
47
|
+
path: TEST_FILE,
|
|
48
|
+
oldText: "missing",
|
|
49
|
+
newText: "found",
|
|
50
|
+
};
|
|
51
|
+
expect(() => handleReplaceText(TEST_DIR, args)).toThrowError("Text not found in file");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should throw an error for ambiguous replacement if allowMultiple is false", () => {
|
|
55
|
+
fs.writeFileSync(TEST_FILE, "hello world world", "utf8");
|
|
56
|
+
const args: ToolArgs = {
|
|
57
|
+
path: TEST_FILE,
|
|
58
|
+
oldText: "world",
|
|
59
|
+
newText: "there",
|
|
60
|
+
allowMultiple: false,
|
|
61
|
+
};
|
|
62
|
+
expect(() => handleReplaceText(TEST_DIR, args)).toThrowError("Ambiguous replacement");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should replace all occurrences of text if allowMultiple is true", () => {
|
|
66
|
+
fs.writeFileSync(TEST_FILE, "hello world world", "utf8");
|
|
67
|
+
const args: ToolArgs = {
|
|
68
|
+
path: TEST_FILE,
|
|
69
|
+
oldText: "world",
|
|
70
|
+
newText: "there",
|
|
71
|
+
allowMultiple: true,
|
|
72
|
+
};
|
|
73
|
+
const result = handleReplaceText(TEST_DIR, args as any);
|
|
74
|
+
expect(result.content[0].text).toContain("✅ Surgical edit successful in");
|
|
75
|
+
expect(fs.readFileSync(TEST_FILE, "utf8")).toBe("hello there there\n");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should log usage to metrics.json upon replace", () => {
|
|
79
|
+
fs.writeFileSync(TEST_FILE, "hello world", "utf8");
|
|
80
|
+
const args: ToolArgs = {
|
|
81
|
+
path: TEST_FILE,
|
|
82
|
+
oldText: "world",
|
|
83
|
+
newText: "there",
|
|
84
|
+
};
|
|
85
|
+
handleReplaceText(TEST_DIR, args);
|
|
86
|
+
const metricsPath = path.join(TEST_DIR, ".enderun/observability/metrics.json");
|
|
87
|
+
expect(fs.existsSync(metricsPath)).toBe(true);
|
|
88
|
+
const metrics = JSON.parse(fs.readFileSync(metricsPath, "utf8"));
|
|
89
|
+
expect(metrics[0].action).toContain("replace_text");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("handlePatchFile", () => {
|
|
94
|
+
it("should replace lines in specified range", () => {
|
|
95
|
+
fs.writeFileSync(TEST_FILE, "line1\nline2\nline3\nline4", "utf8");
|
|
96
|
+
const args: ToolArgs = {
|
|
97
|
+
path: TEST_FILE,
|
|
98
|
+
startLine: 2,
|
|
99
|
+
endLine: 3,
|
|
100
|
+
newContent: "patched2\npatched3",
|
|
101
|
+
};
|
|
102
|
+
const result = handlePatchFile(TEST_DIR, args);
|
|
103
|
+
expect(result.content[0].text).toContain("✅ File patched successfully");
|
|
104
|
+
expect(fs.readFileSync(TEST_FILE, "utf8")).toBe("line1\npatched2\npatched3\nline4");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should throw an error for invalid start line", () => {
|
|
108
|
+
fs.writeFileSync(TEST_FILE, "line1\nline2", "utf8");
|
|
109
|
+
const args: ToolArgs = {
|
|
110
|
+
path: TEST_FILE,
|
|
111
|
+
startLine: 5,
|
|
112
|
+
endLine: 2,
|
|
113
|
+
newContent: "error",
|
|
114
|
+
};
|
|
115
|
+
expect(() => handlePatchFile(TEST_DIR, args)).toThrowError("Invalid start line");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should throw an error for invalid end line", () => {
|
|
119
|
+
fs.writeFileSync(TEST_FILE, "line1\nline2", "utf8");
|
|
120
|
+
const args: ToolArgs = {
|
|
121
|
+
path: TEST_FILE,
|
|
122
|
+
startLine: 1,
|
|
123
|
+
endLine: 5,
|
|
124
|
+
newContent: "error",
|
|
125
|
+
};
|
|
126
|
+
expect(() => handlePatchFile(TEST_DIR, args)).toThrowError("Invalid end line");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should log usage to metrics.json upon patch", () => {
|
|
130
|
+
fs.writeFileSync(TEST_FILE, "line1\nline2\nline3\nline4", "utf8");
|
|
131
|
+
const args: ToolArgs = {
|
|
132
|
+
path: TEST_FILE,
|
|
133
|
+
startLine: 2,
|
|
134
|
+
endLine: 3,
|
|
135
|
+
newContent: "patched2\npatched3",
|
|
136
|
+
};
|
|
137
|
+
handlePatchFile(TEST_DIR, args as any);
|
|
138
|
+
const metricsPath = path.join(TEST_DIR, ".enderun/observability/metrics.json");
|
|
139
|
+
expect(fs.existsSync(metricsPath)).toBe(true);
|
|
140
|
+
const metrics = JSON.parse(fs.readFileSync(metricsPath, "utf8"));
|
|
141
|
+
expect(metrics[0].action).toContain("patch_file");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("handleReadFile", () => {
|
|
146
|
+
it("should read full file content", () => {
|
|
147
|
+
fs.writeFileSync(TEST_FILE, "hello world", "utf8");
|
|
148
|
+
const args: ToolArgs = { path: TEST_FILE };
|
|
149
|
+
const result = handleReadFile(TEST_DIR, args as any);
|
|
150
|
+
expect(result.content[0].text).toBe("hello world");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should read sliced lines", () => {
|
|
154
|
+
fs.writeFileSync(TEST_FILE, "line1\nline2\nline3\nline4", "utf8");
|
|
155
|
+
const args: ToolArgs = {
|
|
156
|
+
path: TEST_FILE,
|
|
157
|
+
startLine: 2,
|
|
158
|
+
endLine: 3
|
|
159
|
+
};
|
|
160
|
+
const result = handleReadFile(TEST_DIR, args as any);
|
|
161
|
+
expect(result.content[0].text).toBe("line2\nline3");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should truncate long files", () => {
|
|
165
|
+
const longContent = Array.from({ length: 1005 }, (_, i) => `line ${i + 1}`).join("\n");
|
|
166
|
+
fs.writeFileSync(TEST_FILE, longContent, "utf8");
|
|
167
|
+
const args: ToolArgs = { path: TEST_FILE };
|
|
168
|
+
const result = handleReadFile(TEST_DIR, args as any);
|
|
169
|
+
expect(result.content[0].text).toContain("TRUNCATED");
|
|
170
|
+
expect(result.content[0].text).toContain("line 1000");
|
|
171
|
+
expect(result.content[0].text).not.toContain("line 1001");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("handleWriteFile", () => {
|
|
176
|
+
it("should write new file and log usage", () => {
|
|
177
|
+
const args: ToolArgs = {
|
|
178
|
+
path: TEST_FILE,
|
|
179
|
+
content: "hello world"
|
|
180
|
+
};
|
|
181
|
+
const result = handleWriteFile(TEST_DIR, args);
|
|
182
|
+
expect(result.content[0].text).toContain("✅ File written:");
|
|
183
|
+
expect(fs.readFileSync(TEST_FILE, "utf8")).toBe("hello world\n");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should write new file and log usage to metrics.json", () => {
|
|
187
|
+
const args: ToolArgs = {
|
|
188
|
+
path: TEST_FILE,
|
|
189
|
+
content: "hello world"
|
|
190
|
+
};
|
|
191
|
+
handleWriteFile(TEST_DIR, args);
|
|
192
|
+
const metricsPath = path.join(TEST_DIR, ".enderun/observability/metrics.json");
|
|
193
|
+
expect(fs.existsSync(metricsPath)).toBe(true);
|
|
194
|
+
const metrics = JSON.parse(fs.readFileSync(metricsPath, "utf8"));
|
|
195
|
+
expect(metrics[0].action).toContain("write_file");
|
|
196
|
+
expect(metrics[0].estimatedTokens).toBeGreaterThan(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should append update to PROJECT_MEMORY.md if it exists", () => {
|
|
200
|
+
fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
|
201
|
+
fs.writeFileSync(MEMORY_FILE, "## CURRENT STATUS\n", "utf8");
|
|
202
|
+
const args: ToolArgs = {
|
|
203
|
+
path: TEST_FILE,
|
|
204
|
+
content: "hello world"
|
|
205
|
+
};
|
|
206
|
+
handleWriteFile(TEST_DIR, args);
|
|
207
|
+
const memoryContent = fs.readFileSync(MEMORY_FILE, "utf8");
|
|
208
|
+
expect(memoryContent).toContain("Auto-Update");
|
|
209
|
+
expect(memoryContent).toContain("wrote file");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { handleSendAgentMessage } from "../../../src/tools/messaging/send_message.js";
|
|
5
|
+
import { ToolArgs } from "../../../src/tools/types.js";
|
|
6
|
+
|
|
7
|
+
const TEST_DIR = path.resolve(__dirname, "../../_temp_messaging_tests");
|
|
8
|
+
const MESSAGES_DIR = path.join(TEST_DIR, ".gemini/messages");
|
|
9
|
+
const BACKEND_LOCK_FILE = path.join(MESSAGES_DIR, "backend.lock");
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
fs.mkdirSync(TEST_DIR, { recursive: true });
|
|
13
|
+
// Write a dummy package.json to test directory to lock frameworkDir as .gemini
|
|
14
|
+
const dummyPkg = {
|
|
15
|
+
name: "test-pkg",
|
|
16
|
+
enderun: {
|
|
17
|
+
frameworkDir: ".gemini"
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
fs.writeFileSync(path.join(TEST_DIR, "package.json"), JSON.stringify(dummyPkg, null, 2), "utf8");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("Hermes Lock Protocol & Message sending", () => {
|
|
28
|
+
it("should successfully send message when lock is not present", async () => {
|
|
29
|
+
const args: ToolArgs = {
|
|
30
|
+
to: "@backend",
|
|
31
|
+
from: "@manager",
|
|
32
|
+
category: "ACTION",
|
|
33
|
+
content: "Build database models",
|
|
34
|
+
traceId: "T-123",
|
|
35
|
+
priority: "HIGH"
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const result = await handleSendAgentMessage(TEST_DIR, args);
|
|
39
|
+
expect(result.content[0].text).toContain("✅ Message sent to @backend");
|
|
40
|
+
|
|
41
|
+
// Verify the message file contains the sent message
|
|
42
|
+
const messageFilePath = path.join(MESSAGES_DIR, "backend.json");
|
|
43
|
+
expect(fs.existsSync(messageFilePath)).toBe(true);
|
|
44
|
+
|
|
45
|
+
const content = fs.readFileSync(messageFilePath, "utf8");
|
|
46
|
+
const msg = JSON.parse(content.trim());
|
|
47
|
+
expect(msg.from).toBe("@manager");
|
|
48
|
+
expect(msg.to).toBe("@backend");
|
|
49
|
+
expect(msg.category).toBe("ACTION");
|
|
50
|
+
expect(msg.content).toBe("Build database models");
|
|
51
|
+
expect(msg.traceId).toBe("T-123");
|
|
52
|
+
expect(msg.priority).toBe("HIGH");
|
|
53
|
+
expect(msg.status).toBe("PENDING");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should retry and fail to send message if lock is kept busy", async () => {
|
|
57
|
+
// Prepare a busy lock file
|
|
58
|
+
fs.mkdirSync(MESSAGES_DIR, { recursive: true });
|
|
59
|
+
fs.writeFileSync(BACKEND_LOCK_FILE, "Locked by @test at " + new Date().toISOString(), "utf8");
|
|
60
|
+
|
|
61
|
+
const args: ToolArgs = {
|
|
62
|
+
to: "@backend",
|
|
63
|
+
from: "@manager",
|
|
64
|
+
category: "ACTION",
|
|
65
|
+
content: "Build database models",
|
|
66
|
+
traceId: "T-123"
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const startTime = Date.now();
|
|
70
|
+
const result = await handleSendAgentMessage(TEST_DIR, args);
|
|
71
|
+
const duration = Date.now() - startTime;
|
|
72
|
+
|
|
73
|
+
expect(result.isError).toBe(true);
|
|
74
|
+
expect(result.content[0].text).toContain("❌ Could not send message to @backend: Hermes lock is busy.");
|
|
75
|
+
// Retries takes 3 attempts with 500ms delay -> duration should be at least ~1000ms
|
|
76
|
+
expect(duration).toBeGreaterThanOrEqual(1000);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should bypass and acquire lock if existing lock is stale (older than 5s)", async () => {
|
|
80
|
+
fs.mkdirSync(MESSAGES_DIR, { recursive: true });
|
|
81
|
+
// Set lock file mtime to 10 seconds ago
|
|
82
|
+
fs.writeFileSync(BACKEND_LOCK_FILE, "Locked by @test at " + new Date().toISOString(), "utf8");
|
|
83
|
+
const tenSecondsAgo = new Date(Date.now() - 10000);
|
|
84
|
+
fs.utimesSync(BACKEND_LOCK_FILE, tenSecondsAgo, tenSecondsAgo);
|
|
85
|
+
|
|
86
|
+
const args: ToolArgs = {
|
|
87
|
+
to: "@backend",
|
|
88
|
+
from: "@manager",
|
|
89
|
+
category: "ACTION",
|
|
90
|
+
content: "Build database models",
|
|
91
|
+
traceId: "T-123"
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const result = await handleSendAgentMessage(TEST_DIR, args);
|
|
95
|
+
expect(result.content[0].text).toContain("✅ Message sent to @backend");
|
|
96
|
+
expect(fs.existsSync(BACKEND_LOCK_FILE)).toBe(false); // Lock should have been unlinked in finally
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should safely resolve concurrent messages and execute them in sequence", async () => {
|
|
100
|
+
const send1Promise = handleSendAgentMessage(TEST_DIR, {
|
|
101
|
+
to: "@backend",
|
|
102
|
+
from: "@manager",
|
|
103
|
+
category: "ACTION",
|
|
104
|
+
content: "Job 1",
|
|
105
|
+
traceId: "T-123"
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Delay starting the second call slightly to make sure lock is acquired by Job 1 first
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
110
|
+
|
|
111
|
+
const send2Promise = handleSendAgentMessage(TEST_DIR, {
|
|
112
|
+
to: "@backend",
|
|
113
|
+
from: "@frontend",
|
|
114
|
+
category: "ACTION",
|
|
115
|
+
content: "Job 2",
|
|
116
|
+
traceId: "T-123"
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const [res1, res2] = await Promise.all([send1Promise, send2Promise]);
|
|
120
|
+
|
|
121
|
+
expect(res1.isError).toBeFalsy();
|
|
122
|
+
expect(res2.isError).toBeFalsy();
|
|
123
|
+
expect(res1.content[0].text).toContain("✅ Message sent to @backend");
|
|
124
|
+
expect(res2.content[0].text).toContain("✅ Message sent to @backend");
|
|
125
|
+
|
|
126
|
+
// Verify both messages exist in order in the message log file
|
|
127
|
+
const messageFilePath = path.join(MESSAGES_DIR, "backend.json");
|
|
128
|
+
const lines = fs.readFileSync(messageFilePath, "utf8").trim().split("\n");
|
|
129
|
+
expect(lines.length).toBe(2);
|
|
130
|
+
|
|
131
|
+
const msg1 = JSON.parse(lines[0]);
|
|
132
|
+
const msg2 = JSON.parse(lines[1]);
|
|
133
|
+
expect(msg1.content).toBe("Job 1");
|
|
134
|
+
expect(msg2.content).toBe("Job 2");
|
|
135
|
+
});
|
|
136
|
+
});
|