agent-enderun 0.8.8 → 0.9.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/.enderun/BRAIN_DASHBOARD.md +5 -5
- package/{ENDERUN.md → .enderun/ENDERUN.md} +4 -2
- package/.enderun/PROJECT_MEMORY.md +32 -43
- package/.enderun/STATUS.md +25 -28
- package/.enderun/agents/agent_army_schema.json +29 -7
- package/.enderun/agents/backend.md +48 -39
- package/.enderun/agents/database.md +55 -0
- package/.enderun/agents/devops.md +30 -17
- package/.enderun/agents/explorer.md +5 -5
- package/.enderun/agents/frontend.md +97 -76
- package/.enderun/agents/git.md +4 -4
- package/.enderun/agents/manager.md +68 -59
- package/.enderun/agents/mobile.md +4 -4
- package/.enderun/agents/native.md +106 -0
- package/.enderun/agents/quality.md +122 -0
- package/.enderun/agents/schema/agent-lifecycle-schema.json +29 -8
- package/.enderun/cli-commands.json +7 -3
- package/.enderun/config.json +8 -2
- package/.enderun/knowledge/SHIM_TEMPLATE.md +25 -0
- package/.enderun/knowledge/context_boundary_rules.md +57 -0
- package/.enderun/knowledge/corporate-governance/high-risk-action-approval-flow.md +29 -0
- package/.enderun/knowledge/documentation_ownership.md +53 -0
- package/.enderun/knowledge/eslint-standards.md +63 -0
- package/.enderun/knowledge/frontend_professionalization_guidelines.md +7 -7
- package/.enderun/knowledge/hermes_live_test_guidelines.md +5 -5
- package/.enderun/knowledge/hermes_protocol.md +1 -1
- package/.enderun/knowledge/lessons-learned.md +14 -0
- package/.enderun/knowledge/manager_authority_audit_enforcement.md +10 -10
- package/.enderun/knowledge/project_scaffold_guidelines.md +4 -4
- package/.enderun/knowledge/reference_application_guidelines.md +6 -6
- package/.enderun/mcp_config.json +9 -0
- package/.enderun/memory-graph/agent-contexts/backend.json +1 -1
- package/.enderun/memory-graph/agent-contexts/devops.json +1 -1
- package/.enderun/memory-graph/agent-contexts/explorer.json +1 -1
- package/.enderun/memory-graph/agent-contexts/frontend.json +1 -1
- package/.enderun/memory-graph/agent-contexts/git.json +1 -1
- package/.enderun/memory-graph/agent-contexts/manager.json +1 -1
- package/.enderun/memory-graph/agent-contexts/mobile.json +1 -1
- package/.enderun/memory-graph/agent-contexts/native.json +1 -0
- package/.enderun/memory-graph/agent-contexts/quality.json +1 -0
- package/.enderun/memory-graph/graph.json +123 -15
- package/.enderun/memory-graph/shared-facts.json +26 -31
- package/.enderun/queue/README.md +13 -13
- package/README.md +171 -393
- package/agent.md +25 -0
- package/bin/cli.js +10 -10
- package/bin/init-check.js +69 -0
- package/bin/update-contract.js +57 -38
- package/bin/validate-agent-army.js +38 -13
- package/claude.md +14 -8
- package/docs/README.md +24 -16
- package/docs/architecture/README.md +9 -0
- package/docs/architecture/standards/data-fetching-patterns.md +13 -0
- package/docs/architecture/standards/design-system.md +31 -0
- package/docs/architecture/standards/logging.md +7 -0
- package/docs/architecture/standards/tech-stack.md +9 -0
- package/docs/getting-started.md +9 -259
- package/docs/user/README.md +35 -0
- package/docs/user/action-plan-2026.md +9 -0
- package/docs/user/getting-started.md +13 -0
- package/docs/user/roadmap.md +13 -0
- package/eslint.config.js +68 -0
- package/framework-mcp/dist/index.js +178 -24
- package/framework-mcp/package.json +6 -33
- package/framework-mcp/tsconfig.json +3 -6
- package/gemini.md +12 -6
- package/grok.md +25 -0
- package/package.json +13 -5
- package/panda.config.ts +86 -86
- package/src/cli/adapters.ts +208 -0
- package/src/cli/commands/app.ts +38 -0
- package/src/cli/commands/check.ts +87 -0
- package/src/cli/commands/compliance.ts +55 -0
- package/src/cli/commands/contract.ts +45 -0
- package/src/cli/commands/explorer.ts +45 -0
- package/src/cli/commands/git.ts +39 -0
- package/src/cli/commands/init.ts +272 -0
- package/src/cli/commands/knowledge.ts +44 -0
- package/src/cli/commands/lint.ts +25 -0
- package/src/cli/commands/log.ts +37 -0
- package/src/cli/commands/memory.ts +78 -0
- package/src/cli/commands/orchestrate.ts +111 -0
- package/src/cli/commands/script.ts +20 -0
- package/src/cli/commands/security.ts +38 -0
- package/src/cli/commands/status.ts +59 -0
- package/src/cli/commands/trace.ts +46 -0
- package/src/cli/index.ts +74 -2155
- package/src/cli/utils/app.ts +764 -0
- package/src/cli/utils/claude.ts +56 -0
- package/src/cli/utils/fs.ts +139 -0
- package/src/cli/utils/memory.ts +141 -0
- package/src/cli/utils/pkg.ts +215 -0
- package/src/cli/utils/string.ts +48 -0
- package/src/cli/utils/time.ts +27 -0
- package/tsconfig.json +9 -0
- package/.enderun/agents/analyst.md +0 -238
- package/.enderun/agents/orchestrator.md +0 -172
- package/.enderun/agents/qa.md +0 -124
- package/.enderun/agents/security.md +0 -202
- package/.enderun/knowledge/api_design_rules.md +0 -6
- package/.enderun/knowledge/branded_types_pattern.md +0 -8
- package/.enderun/knowledge/code_review_checklist.md +0 -7
- package/.enderun/knowledge/contract_versioning.md +0 -7
- package/.enderun/knowledge/database_migration.md +0 -6
- package/.enderun/knowledge/deployment_checklist.md +0 -7
- package/.enderun/knowledge/git_commit_strategy.md +0 -10
- package/.enderun/knowledge/monitoring_setup.md +0 -5
- package/.enderun/knowledge/performance_guidelines.md +0 -11
- package/.enderun/knowledge/repository_patterns.md +0 -9
- package/.enderun/knowledge/security_scanning.md +0 -6
- package/.enderun/knowledge/testing_standards.md +0 -7
- package/.enderun/knowledge/troubleshooting_guide.md +0 -5
- package/.enderun/logs/analyst.json +0 -1
- package/.enderun/logs/backend.json +0 -1
- package/.enderun/logs/devops.json +0 -1
- package/.enderun/logs/explorer.json +0 -1
- package/.enderun/logs/frontend.json +0 -1
- package/.enderun/logs/git.json +0 -1
- package/.enderun/logs/manager.json +0 -1
- package/.enderun/logs/mobile.json +0 -1
- package/.enderun/logs/native.json +0 -1
- package/.enderun/logs/orchestrator.json +0 -1
- package/.enderun/logs/qa.json +0 -1
- package/.enderun/logs/security.json +0 -1
- package/.enderun/memory-graph/agent-contexts/analyst.json +0 -1
- package/.enderun/memory-graph/agent-contexts/orchestrator.json +0 -1
- package/.enderun/memory-graph/agent-contexts/qa.json +0 -1
- package/.enderun/memory-graph/agent-contexts/security.json +0 -1
- package/.env.example +0 -10
- package/cursor.md +0 -19
- package/docs/action-plan-2026.md +0 -119
- package/docs/roadmap.md +0 -142
- package/framework-mcp/README.md +0 -55
- package/framework-mcp/dist/schemas.js +0 -84
- package/framework-mcp/dist/tools/codebase.js +0 -294
- package/framework-mcp/dist/tools/contract.js +0 -197
- package/framework-mcp/dist/tools/framework.js +0 -225
- package/framework-mcp/dist/tools/git.js +0 -51
- package/framework-mcp/dist/tools/governance.js +0 -696
- package/framework-mcp/dist/tools/index.js +0 -47
- package/framework-mcp/dist/tools/knowledge.js +0 -133
- package/framework-mcp/dist/tools/memory.js +0 -217
- package/framework-mcp/dist/tools/messages.js +0 -193
- package/framework-mcp/dist/tools/monitoring.js +0 -294
- package/framework-mcp/dist/tools/orchestration.js +0 -369
- package/framework-mcp/dist/tools/pipeline.js +0 -267
- package/framework-mcp/dist/tools/security.js +0 -137
- package/framework-mcp/dist/utils.js +0 -121
- package/framework-mcp/src/index.ts +0 -49
- package/framework-mcp/src/schemas.ts +0 -106
- package/framework-mcp/src/tools/codebase.ts +0 -284
- package/framework-mcp/src/tools/contract.ts +0 -221
- package/framework-mcp/src/tools/framework.ts +0 -223
- package/framework-mcp/src/tools/git.ts +0 -42
- package/framework-mcp/src/tools/governance.ts +0 -891
- package/framework-mcp/src/tools/index.ts +0 -50
- package/framework-mcp/src/tools/knowledge.ts +0 -141
- package/framework-mcp/src/tools/memory.ts +0 -207
- package/framework-mcp/src/tools/messages.ts +0 -213
- package/framework-mcp/src/tools/monitoring.ts +0 -351
- package/framework-mcp/src/tools/orchestration.ts +0 -440
- package/framework-mcp/src/tools/pipeline.ts +0 -353
- package/framework-mcp/src/tools/security.ts +0 -143
- package/framework-mcp/src/utils.ts +0 -130
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
import { promises as fs } from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
|
|
5
|
-
// ─── Types ─────────────────────────────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
type PipelineStage = {
|
|
8
|
-
id: string;
|
|
9
|
-
name: string;
|
|
10
|
-
agent: string;
|
|
11
|
-
status: "PENDING" | "RUNNING" | "COMPLETED" | "FAILED" | "SKIPPED";
|
|
12
|
-
dependsOn: string[];
|
|
13
|
-
startedAt?: string;
|
|
14
|
-
completedAt?: string;
|
|
15
|
-
output?: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
type Pipeline = {
|
|
19
|
-
id: string;
|
|
20
|
-
traceId: string;
|
|
21
|
-
name: string;
|
|
22
|
-
description: string;
|
|
23
|
-
createdAt: string;
|
|
24
|
-
updatedAt: string;
|
|
25
|
-
status: "PENDING" | "RUNNING" | "COMPLETED" | "FAILED" | "ROLLED_BACK";
|
|
26
|
-
currentStageId?: string;
|
|
27
|
-
stages: PipelineStage[];
|
|
28
|
-
rollbackStages?: PipelineStage[];
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// ─── Tool Definitions ─────────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
export const pipelineTools: Tool[] = [
|
|
34
|
-
{
|
|
35
|
-
name: "create_execution_pipeline",
|
|
36
|
-
description:
|
|
37
|
-
"Creates a multi-stage execution pipeline for complex workflows (e.g., build → test → security audit → deploy). Each stage is assigned to a specific agent and has defined dependencies. The pipeline is persisted and can be advanced or rolled back.",
|
|
38
|
-
inputSchema: {
|
|
39
|
-
type: "object",
|
|
40
|
-
properties: {
|
|
41
|
-
traceId: { type: "string", description: "Active Trace ID from @manager briefing" },
|
|
42
|
-
name: { type: "string", description: "Pipeline name (e.g. 'Production Deploy v1.2.0')" },
|
|
43
|
-
description: { type: "string", description: "What this pipeline accomplishes" },
|
|
44
|
-
stages: {
|
|
45
|
-
type: "array",
|
|
46
|
-
description: "Ordered pipeline stages",
|
|
47
|
-
items: {
|
|
48
|
-
type: "object",
|
|
49
|
-
required: ["id", "name", "agent"],
|
|
50
|
-
properties: {
|
|
51
|
-
id: { type: "string" },
|
|
52
|
-
name: { type: "string" },
|
|
53
|
-
agent: { type: "string" },
|
|
54
|
-
dependsOn: { type: "array", items: { type: "string" } },
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
rollbackStages: {
|
|
59
|
-
type: "array",
|
|
60
|
-
description: "Optional rollback stages executed in reverse order on pipeline failure",
|
|
61
|
-
items: {
|
|
62
|
-
type: "object",
|
|
63
|
-
required: ["id", "name", "agent"],
|
|
64
|
-
properties: {
|
|
65
|
-
id: { type: "string" },
|
|
66
|
-
name: { type: "string" },
|
|
67
|
-
agent: { type: "string" },
|
|
68
|
-
dependsOn: { type: "array", items: { type: "string" } },
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
required: ["traceId", "name", "stages"],
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: "advance_pipeline_stage",
|
|
78
|
-
description:
|
|
79
|
-
"Advances a pipeline to the next eligible stage. Marks the current stage as COMPLETED and activates all dependent stages that now have their dependencies satisfied. Returns the new pipeline state.",
|
|
80
|
-
inputSchema: {
|
|
81
|
-
type: "object",
|
|
82
|
-
properties: {
|
|
83
|
-
pipelineId: { type: "string", description: "Pipeline ID" },
|
|
84
|
-
completedStageId: { type: "string", description: "The stage that just completed successfully" },
|
|
85
|
-
stageOutput: { type: "string", description: "Optional output/result from the completed stage" },
|
|
86
|
-
},
|
|
87
|
-
required: ["pipelineId", "completedStageId"],
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
name: "rollback_pipeline",
|
|
92
|
-
description:
|
|
93
|
-
"Rolls back a pipeline to the last stable state by executing the defined rollback stages in reverse order. Updates the pipeline status to ROLLED_BACK.",
|
|
94
|
-
inputSchema: {
|
|
95
|
-
type: "object",
|
|
96
|
-
properties: {
|
|
97
|
-
pipelineId: { type: "string", description: "Pipeline ID to roll back" },
|
|
98
|
-
reason: { type: "string", description: "Reason for the rollback (mandatory for audit)" },
|
|
99
|
-
},
|
|
100
|
-
required: ["pipelineId", "reason"],
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
name: "get_pipeline_status",
|
|
105
|
-
description:
|
|
106
|
-
"Returns the full current status of a pipeline including all stage states, progress percentage, blocking stages, and estimated completion.",
|
|
107
|
-
inputSchema: {
|
|
108
|
-
type: "object",
|
|
109
|
-
properties: {
|
|
110
|
-
pipelineId: { type: "string", description: "Pipeline ID" },
|
|
111
|
-
},
|
|
112
|
-
required: ["pipelineId"],
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
// ─── Handlers ─────────────────────────────────────────────────────────────────
|
|
118
|
-
|
|
119
|
-
export const pipelineHandlers: Record<
|
|
120
|
-
string,
|
|
121
|
-
(args: unknown, projectRoot: string) => Promise<{ content: Array<{ type: string; text: string }> }>
|
|
122
|
-
> = {
|
|
123
|
-
async create_execution_pipeline(args, projectRoot) {
|
|
124
|
-
const { traceId, name, description, stages, rollbackStages } = args as {
|
|
125
|
-
traceId: string;
|
|
126
|
-
name: string;
|
|
127
|
-
description?: string;
|
|
128
|
-
stages: Omit<PipelineStage, "status" | "startedAt" | "completedAt" | "output">[];
|
|
129
|
-
rollbackStages?: Omit<PipelineStage, "status" | "startedAt" | "completedAt" | "output">[];
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const pipelineId = `PIPE-${traceId}-${Date.now()}`;
|
|
133
|
-
const now = new Date().toISOString();
|
|
134
|
-
|
|
135
|
-
const pipeline: Pipeline = {
|
|
136
|
-
id: pipelineId,
|
|
137
|
-
traceId,
|
|
138
|
-
name,
|
|
139
|
-
description: description ?? "",
|
|
140
|
-
createdAt: now,
|
|
141
|
-
updatedAt: now,
|
|
142
|
-
status: "PENDING",
|
|
143
|
-
stages: stages.map((s) => ({
|
|
144
|
-
...s,
|
|
145
|
-
dependsOn: s.dependsOn ?? [],
|
|
146
|
-
status: "PENDING" as const,
|
|
147
|
-
})),
|
|
148
|
-
rollbackStages: rollbackStages?.map((s) => ({
|
|
149
|
-
...s,
|
|
150
|
-
dependsOn: s.dependsOn ?? [],
|
|
151
|
-
status: "PENDING" as const,
|
|
152
|
-
})),
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
// Activate stages with no dependencies
|
|
156
|
-
for (const stage of pipeline.stages) {
|
|
157
|
-
if (stage.dependsOn.length === 0) {
|
|
158
|
-
stage.status = "RUNNING";
|
|
159
|
-
stage.startedAt = now;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
pipeline.status = "RUNNING";
|
|
163
|
-
|
|
164
|
-
// Persist pipeline
|
|
165
|
-
const pipelineDir = path.join(projectRoot, ".enderun", "queue", "pipelines");
|
|
166
|
-
await fs.mkdir(pipelineDir, { recursive: true });
|
|
167
|
-
await fs.writeFile(
|
|
168
|
-
path.join(pipelineDir, `${pipelineId}.json`),
|
|
169
|
-
JSON.stringify(pipeline, null, 2),
|
|
170
|
-
"utf-8"
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
return { content: [{ type: "text", text: JSON.stringify(pipeline, null, 2) }] };
|
|
174
|
-
},
|
|
175
|
-
|
|
176
|
-
async advance_pipeline_stage(args, projectRoot) {
|
|
177
|
-
const { pipelineId, completedStageId, stageOutput } = args as {
|
|
178
|
-
pipelineId: string;
|
|
179
|
-
completedStageId: string;
|
|
180
|
-
stageOutput?: string;
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const pipelineDir = path.join(projectRoot, ".enderun", "queue", "pipelines");
|
|
184
|
-
const pipelinePath = path.join(pipelineDir, `${pipelineId}.json`);
|
|
185
|
-
|
|
186
|
-
let pipeline: Pipeline;
|
|
187
|
-
try {
|
|
188
|
-
const content = await fs.readFile(pipelinePath, "utf-8");
|
|
189
|
-
pipeline = JSON.parse(content) as Pipeline;
|
|
190
|
-
} catch {
|
|
191
|
-
return {
|
|
192
|
-
content: [
|
|
193
|
-
{ type: "text", text: JSON.stringify({ error: "PIPELINE_NOT_FOUND", pipelineId }) },
|
|
194
|
-
],
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const now = new Date().toISOString();
|
|
199
|
-
|
|
200
|
-
// Mark completed stage
|
|
201
|
-
const completedStage = pipeline.stages.find((s) => s.id === completedStageId);
|
|
202
|
-
if (!completedStage) {
|
|
203
|
-
return {
|
|
204
|
-
content: [
|
|
205
|
-
{ type: "text", text: JSON.stringify({ error: "STAGE_NOT_FOUND", completedStageId }) },
|
|
206
|
-
],
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
completedStage.status = "COMPLETED";
|
|
210
|
-
completedStage.completedAt = now;
|
|
211
|
-
if (stageOutput) completedStage.output = stageOutput;
|
|
212
|
-
|
|
213
|
-
// Activate newly unblocked stages
|
|
214
|
-
const completedIds = new Set(pipeline.stages.filter((s) => s.status === "COMPLETED").map((s) => s.id));
|
|
215
|
-
const newlyActivated: string[] = [];
|
|
216
|
-
|
|
217
|
-
for (const stage of pipeline.stages) {
|
|
218
|
-
if (stage.status === "PENDING" && stage.dependsOn.every((dep) => completedIds.has(dep))) {
|
|
219
|
-
stage.status = "RUNNING";
|
|
220
|
-
stage.startedAt = now;
|
|
221
|
-
newlyActivated.push(stage.id);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Check if pipeline is complete
|
|
226
|
-
const allDone = pipeline.stages.every((s) => s.status === "COMPLETED");
|
|
227
|
-
if (allDone) {
|
|
228
|
-
pipeline.status = "COMPLETED";
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
pipeline.updatedAt = now;
|
|
232
|
-
await fs.writeFile(pipelinePath, JSON.stringify(pipeline, null, 2), "utf-8");
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
content: [
|
|
236
|
-
{
|
|
237
|
-
type: "text",
|
|
238
|
-
text: JSON.stringify({ pipeline, newlyActivatedStages: newlyActivated }, null, 2),
|
|
239
|
-
},
|
|
240
|
-
],
|
|
241
|
-
};
|
|
242
|
-
},
|
|
243
|
-
|
|
244
|
-
async rollback_pipeline(args, projectRoot) {
|
|
245
|
-
const { pipelineId, reason } = args as { pipelineId: string; reason: string };
|
|
246
|
-
|
|
247
|
-
const pipelineDir = path.join(projectRoot, ".enderun", "queue", "pipelines");
|
|
248
|
-
const pipelinePath = path.join(pipelineDir, `${pipelineId}.json`);
|
|
249
|
-
|
|
250
|
-
let pipeline: Pipeline;
|
|
251
|
-
try {
|
|
252
|
-
const content = await fs.readFile(pipelinePath, "utf-8");
|
|
253
|
-
pipeline = JSON.parse(content) as Pipeline;
|
|
254
|
-
} catch {
|
|
255
|
-
return {
|
|
256
|
-
content: [
|
|
257
|
-
{ type: "text", text: JSON.stringify({ error: "PIPELINE_NOT_FOUND", pipelineId }) },
|
|
258
|
-
],
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const now = new Date().toISOString();
|
|
263
|
-
pipeline.status = "ROLLED_BACK";
|
|
264
|
-
pipeline.updatedAt = now;
|
|
265
|
-
|
|
266
|
-
// Mark all non-completed stages as SKIPPED
|
|
267
|
-
for (const stage of pipeline.stages) {
|
|
268
|
-
if (stage.status === "RUNNING" || stage.status === "PENDING") {
|
|
269
|
-
stage.status = "SKIPPED";
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Activate rollback stages if defined
|
|
274
|
-
if (pipeline.rollbackStages) {
|
|
275
|
-
for (const stage of pipeline.rollbackStages) {
|
|
276
|
-
stage.status = "RUNNING";
|
|
277
|
-
stage.startedAt = now;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
await fs.writeFile(pipelinePath, JSON.stringify(pipeline, null, 2), "utf-8");
|
|
282
|
-
|
|
283
|
-
// Write rollback event to devops log
|
|
284
|
-
const devopsLogPath = path.join(projectRoot, ".enderun", "logs", "devops.json");
|
|
285
|
-
let devopsLog: { rollbacks: unknown[] } = { rollbacks: [] };
|
|
286
|
-
try {
|
|
287
|
-
const existing = await fs.readFile(devopsLogPath, "utf-8");
|
|
288
|
-
devopsLog = JSON.parse(existing) as typeof devopsLog;
|
|
289
|
-
if (!devopsLog.rollbacks) devopsLog.rollbacks = [];
|
|
290
|
-
} catch {
|
|
291
|
-
// Start fresh
|
|
292
|
-
}
|
|
293
|
-
devopsLog.rollbacks.push({ pipelineId, reason, timestamp: now });
|
|
294
|
-
await fs.mkdir(path.dirname(devopsLogPath), { recursive: true });
|
|
295
|
-
await fs.writeFile(devopsLogPath, JSON.stringify(devopsLog, null, 2), "utf-8");
|
|
296
|
-
|
|
297
|
-
return { content: [{ type: "text", text: JSON.stringify({ pipeline, rollbackReason: reason }, null, 2) }] };
|
|
298
|
-
},
|
|
299
|
-
|
|
300
|
-
async get_pipeline_status(args, projectRoot) {
|
|
301
|
-
const { pipelineId } = args as { pipelineId: string };
|
|
302
|
-
const pipelinePath = path.join(
|
|
303
|
-
projectRoot,
|
|
304
|
-
".enderun",
|
|
305
|
-
"queue",
|
|
306
|
-
"pipelines",
|
|
307
|
-
`${pipelineId}.json`
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
try {
|
|
311
|
-
const content = await fs.readFile(pipelinePath, "utf-8");
|
|
312
|
-
const pipeline = JSON.parse(content) as Pipeline;
|
|
313
|
-
|
|
314
|
-
const total = pipeline.stages.length;
|
|
315
|
-
const completed = pipeline.stages.filter((s) => s.status === "COMPLETED").length;
|
|
316
|
-
const running = pipeline.stages.filter((s) => s.status === "RUNNING").map((s) => s.id);
|
|
317
|
-
const blocked = pipeline.stages.filter(
|
|
318
|
-
(s) => s.status === "PENDING" && s.dependsOn.some((dep) => {
|
|
319
|
-
const depStage = pipeline.stages.find((st) => st.id === dep);
|
|
320
|
-
return depStage && depStage.status !== "COMPLETED";
|
|
321
|
-
})
|
|
322
|
-
).map((s) => s.id);
|
|
323
|
-
|
|
324
|
-
return {
|
|
325
|
-
content: [
|
|
326
|
-
{
|
|
327
|
-
type: "text",
|
|
328
|
-
text: JSON.stringify(
|
|
329
|
-
{
|
|
330
|
-
pipelineId,
|
|
331
|
-
name: pipeline.name,
|
|
332
|
-
status: pipeline.status,
|
|
333
|
-
progress: `${completed}/${total} stages (${Math.round((completed / total) * 100)}%)`,
|
|
334
|
-
currentlyRunning: running,
|
|
335
|
-
waitingOn: blocked,
|
|
336
|
-
createdAt: pipeline.createdAt,
|
|
337
|
-
updatedAt: pipeline.updatedAt,
|
|
338
|
-
},
|
|
339
|
-
null,
|
|
340
|
-
2
|
|
341
|
-
),
|
|
342
|
-
},
|
|
343
|
-
],
|
|
344
|
-
};
|
|
345
|
-
} catch {
|
|
346
|
-
return {
|
|
347
|
-
content: [
|
|
348
|
-
{ type: "text", text: JSON.stringify({ error: "PIPELINE_NOT_FOUND", pipelineId }) },
|
|
349
|
-
],
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
},
|
|
353
|
-
};
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { Project, SyntaxKind } from "ts-morph";
|
|
4
|
-
import {
|
|
5
|
-
collectFilesRecursively,
|
|
6
|
-
resolveSafePath,
|
|
7
|
-
buildLineMatches
|
|
8
|
-
} from "../utils.js";
|
|
9
|
-
import {
|
|
10
|
-
SECURITY_AUDIT_ARGS_SCHEMA,
|
|
11
|
-
ANALYZE_CONSTITUTION_COMPLIANCE_ARGS_SCHEMA
|
|
12
|
-
} from "../schemas.js";
|
|
13
|
-
|
|
14
|
-
export const securityTools = [
|
|
15
|
-
{
|
|
16
|
-
name: "security_audit_scan",
|
|
17
|
-
description: "Scans the codebase for security vulnerabilities like hardcoded secrets, raw SQL, and unsafe async patterns.",
|
|
18
|
-
inputSchema: {
|
|
19
|
-
type: "object",
|
|
20
|
-
properties: {
|
|
21
|
-
path: {
|
|
22
|
-
type: "string",
|
|
23
|
-
description: "Path to scan (relative to project root)",
|
|
24
|
-
default: ".",
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
name: "analyze_constitution_compliance",
|
|
31
|
-
description: "Scans a file or directory for violations of the project's ENDERUN.md rules (e.g. Zero UI Library, Branded Types).",
|
|
32
|
-
inputSchema: {
|
|
33
|
-
type: "object",
|
|
34
|
-
properties: {
|
|
35
|
-
path: {
|
|
36
|
-
type: "string",
|
|
37
|
-
description: "Path to scan (relative to project root)",
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
required: ["path"],
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
export const securityHandlers = {
|
|
46
|
-
security_audit_scan: async (args: unknown, projectRoot: string) => {
|
|
47
|
-
const parsed = SECURITY_AUDIT_ARGS_SCHEMA.safeParse(args ?? {});
|
|
48
|
-
if (!parsed.success) return { content: [{ type: "text", text: "Invalid path argument." }] };
|
|
49
|
-
const vulnerabilities: string[] = [];
|
|
50
|
-
const scanRules = [
|
|
51
|
-
{ pattern: /sql`/, message: "Potential Raw SQL usage detected (check Kysely usage)", severity: "HIGH" },
|
|
52
|
-
{ pattern: /(?<!\/\/\s*)console\.log/, message: "console.log found in production code", severity: "LOW" },
|
|
53
|
-
{ pattern: /(password|secret|api_?key)\s*[:=]\s*['"][^'"]+['"]/i, message: "Potential hardcoded secret/password detected", severity: "CRITICAL" },
|
|
54
|
-
{ pattern: /:\s*any(?!\w)/, message: "Usage of 'any' type detected", severity: "MEDIUM" },
|
|
55
|
-
{ pattern: /(?<!\w)eval\s*\(/, message: "Dangerous 'eval()' usage detected", severity: "HIGH" },
|
|
56
|
-
{ pattern: /\.innerHTML\s*=/, message: "Unsafe innerHTML assignment detected (XSS risk)", severity: "MEDIUM" },
|
|
57
|
-
{ pattern: /dangerouslySetInnerHTML/, message: "React dangerouslySetInnerHTML detected", severity: "MEDIUM" },
|
|
58
|
-
{ pattern: /TODO:/, message: "Outstanding TODO item found", severity: "LOW" },
|
|
59
|
-
];
|
|
60
|
-
try {
|
|
61
|
-
const safeTargetPath = resolveSafePath(projectRoot, parsed.data.path);
|
|
62
|
-
if (!fs.existsSync(safeTargetPath)) return { content: [{ type: "text", text: "Target path not found." }] };
|
|
63
|
-
const files = collectFilesRecursively(safeTargetPath, new Set(["ts", "tsx"]));
|
|
64
|
-
for (const rule of scanRules) {
|
|
65
|
-
const ruleMatches = buildLineMatches(files, (line) => typeof rule.pattern === "string" ? line.includes(rule.pattern) : rule.pattern.test(line), 5, projectRoot);
|
|
66
|
-
if (ruleMatches.length > 0) vulnerabilities.push(`[${rule.severity}] ${rule.message}:\n${ruleMatches.join("\n")}`);
|
|
67
|
-
}
|
|
68
|
-
const tsProject = new Project({ compilerOptions: { allowJs: true } });
|
|
69
|
-
tsProject.addSourceFilesAtPaths(path.join(safeTargetPath, "**/*.{ts,tsx}"));
|
|
70
|
-
for (const sourceFile of tsProject.getSourceFiles()) {
|
|
71
|
-
const relativePath = path.relative(projectRoot, sourceFile.getFilePath());
|
|
72
|
-
sourceFile.forEachDescendant((node) => {
|
|
73
|
-
if (node.getKindName() === "AnyKeyword") {
|
|
74
|
-
vulnerabilities.push(`[MEDIUM] Precise 'any' type detected in AST at ${relativePath}:${node.getStartLineNumber()}`);
|
|
75
|
-
}
|
|
76
|
-
if (node.getKind() === SyntaxKind.CallExpression) {
|
|
77
|
-
const callExp = node.asKind(SyntaxKind.CallExpression);
|
|
78
|
-
if (callExp?.getExpression().getText() === "console.log") {
|
|
79
|
-
vulnerabilities.push(`[LOW] Production 'console.log' detected in AST at ${relativePath}:${node.getStartLineNumber()}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
return { content: [{ type: "text", text: vulnerabilities.length > 0 ? `### ADVANCED SECURITY AUDIT RESULTS\n\n${Array.from(new Set(vulnerabilities)).join("\n\n")}` : "No security patterns or rule violations detected (Regex & AST verified)." }] };
|
|
85
|
-
} catch {
|
|
86
|
-
return { content: [{ type: "text", text: "Security audit failed." }] };
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
analyze_constitution_compliance: async (args: unknown, projectRoot: string) => {
|
|
90
|
-
const parsed = ANALYZE_CONSTITUTION_COMPLIANCE_ARGS_SCHEMA.safeParse(args ?? {});
|
|
91
|
-
if (!parsed.success) return { content: [{ type: "text", text: "Invalid path argument." }] };
|
|
92
|
-
try {
|
|
93
|
-
const safePath = resolveSafePath(projectRoot, parsed.data.path);
|
|
94
|
-
const violations: string[] = [];
|
|
95
|
-
const forbiddenLibraries = ["@shadcn", "mui", "@chakra-ui", "antd", "bootstrap", "tailwindcss"];
|
|
96
|
-
|
|
97
|
-
if (fs.existsSync(safePath) && fs.lstatSync(safePath).isDirectory()) {
|
|
98
|
-
const tsProject = new Project({ compilerOptions: { allowJs: true } });
|
|
99
|
-
tsProject.addSourceFilesAtPaths(path.join(safePath, "**/*.{ts,tsx,js,jsx}"));
|
|
100
|
-
|
|
101
|
-
for (const sourceFile of tsProject.getSourceFiles()) {
|
|
102
|
-
const relativePath = path.relative(projectRoot, sourceFile.getFilePath());
|
|
103
|
-
if (relativePath.includes("node_modules") || relativePath.includes("dist")) continue;
|
|
104
|
-
|
|
105
|
-
// AST-based Import Check for Zero UI Policy
|
|
106
|
-
sourceFile.getImportDeclarations().forEach(imp => {
|
|
107
|
-
const moduleValue = imp.getModuleSpecifierValue();
|
|
108
|
-
if (forbiddenLibraries.some(lib => moduleValue.includes(lib))) {
|
|
109
|
-
violations.push(`${relativePath}: AST Violation - Zero UI Library Policy (imported '${moduleValue}')`);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// AST-based String ID Check for Branded Types Law
|
|
114
|
-
sourceFile.getInterfaces().forEach(iface => {
|
|
115
|
-
const idProp = iface.getProperty("id");
|
|
116
|
-
if (idProp && idProp.getType().isString() && !idProp.getType().getText().includes("Brand")) {
|
|
117
|
-
violations.push(`${relativePath}: AST Violation - Branded Types Law (interface '${iface.getName()}' uses plain string for ID)`);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const content = sourceFile.getFullText();
|
|
122
|
-
if (content.includes("console.log") && !relativePath.includes("cli.js")) {
|
|
123
|
-
violations.push(`${relativePath}: Text Violation - Logging Policy (found 'console.log')`);
|
|
124
|
-
}
|
|
125
|
-
if (content.includes("sql`")) {
|
|
126
|
-
violations.push(`${relativePath}: Text Violation - Potential Kysely Standards (found raw 'sql' tag)`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
} else if (fs.existsSync(safePath)) {
|
|
130
|
-
// Fallback for single files
|
|
131
|
-
const content = fs.readFileSync(safePath, "utf-8");
|
|
132
|
-
const relativePath = path.relative(projectRoot, safePath);
|
|
133
|
-
for (const lib of forbiddenLibraries) {
|
|
134
|
-
if (content.includes(lib)) violations.push(`${relativePath}: Violation of Zero UI Library Policy (found '${lib}')`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return { content: [{ type: "text", text: `### CONSTITUTION COMPLIANCE REPORT\n\n` + (violations.length > 0 ? `⚠️ **VIOLATIONS FOUND:**\n${violations.map(v => `- ${v}`).join("\n")}` : "✅ **ALL SYSTEMS COMPLIANT:** AST and Regex scans passed. No rule violations detected.") }] };
|
|
139
|
-
} catch {
|
|
140
|
-
return { content: [{ type: "text", text: "Constitution compliance check failed." }] };
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
};
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = path.dirname(__filename);
|
|
7
|
-
|
|
8
|
-
const getPackageVersion = (): string => {
|
|
9
|
-
try {
|
|
10
|
-
const pkgPath = path.join(__dirname, "../package.json");
|
|
11
|
-
if (fs.existsSync(pkgPath)) {
|
|
12
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
13
|
-
return pkg.version || "0.8.8";
|
|
14
|
-
}
|
|
15
|
-
} catch {
|
|
16
|
-
// fallback
|
|
17
|
-
}
|
|
18
|
-
return "0.8.8";
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export const FRAMEWORK_VERSION = getPackageVersion();
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Dynamically detects the framework directory for the current project.
|
|
25
|
-
* Checks in priority order: .enderun → .gemini → .cursor → fallback .gemini
|
|
26
|
-
* This resolves the adapter mismatch bug (KRITIK-1 in v0.7.5 analysis).
|
|
27
|
-
*/
|
|
28
|
-
export function getFrameworkDir(projectRoot: string): string {
|
|
29
|
-
const candidates = [".enderun", ".gemini", ".cursor"];
|
|
30
|
-
for (const dir of candidates) {
|
|
31
|
-
const dirPath = path.join(projectRoot, dir);
|
|
32
|
-
if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
|
|
33
|
-
// Verify it looks like a framework dir (has PROJECT_MEMORY.md or agents/ dir)
|
|
34
|
-
const hasMemory = fs.existsSync(path.join(dirPath, "PROJECT_MEMORY.md"));
|
|
35
|
-
const hasAgents = fs.existsSync(path.join(dirPath, "agents"));
|
|
36
|
-
if (hasMemory || hasAgents) {
|
|
37
|
-
return dir;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
// Ultimate fallback — return first existing dir or .gemini
|
|
42
|
-
for (const dir of candidates) {
|
|
43
|
-
if (fs.existsSync(path.join(projectRoot, dir))) return dir;
|
|
44
|
-
}
|
|
45
|
-
return ".gemini";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function resolveSafePath(projectRoot: string, targetPath: string): string {
|
|
49
|
-
const resolved = path.resolve(projectRoot, targetPath);
|
|
50
|
-
const relative = path.relative(projectRoot, resolved);
|
|
51
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
52
|
-
throw new Error("Path escapes project root.");
|
|
53
|
-
}
|
|
54
|
-
return resolved;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function collectFilesRecursively(targetPath: string, extensions: Set<string>): string[] {
|
|
58
|
-
const results: string[] = [];
|
|
59
|
-
const entries = fs.readdirSync(targetPath, { withFileTypes: true });
|
|
60
|
-
for (const entry of entries) {
|
|
61
|
-
const fullPath = path.join(targetPath, entry.name);
|
|
62
|
-
if (entry.isDirectory()) {
|
|
63
|
-
if (entry.name === "node_modules" || entry.name === ".git")
|
|
64
|
-
continue;
|
|
65
|
-
results.push(...collectFilesRecursively(fullPath, extensions));
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
const ext = path.extname(entry.name).slice(1).toLowerCase();
|
|
69
|
-
if (extensions.has(ext))
|
|
70
|
-
results.push(fullPath);
|
|
71
|
-
}
|
|
72
|
-
return results;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function buildLineMatches(files: string[], matcher: (line: string) => boolean, maxResults: number, projectRoot: string): string[] {
|
|
76
|
-
const matches: string[] = [];
|
|
77
|
-
for (const filePath of files) {
|
|
78
|
-
if (matches.length >= maxResults)
|
|
79
|
-
break;
|
|
80
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
81
|
-
const lines = content.split("\n");
|
|
82
|
-
for (let i = 0; i < lines.length; i++) {
|
|
83
|
-
if (matches.length >= maxResults)
|
|
84
|
-
break;
|
|
85
|
-
const line = lines[i];
|
|
86
|
-
if (!matcher(line))
|
|
87
|
-
continue;
|
|
88
|
-
const relativePath = path.relative(projectRoot, filePath);
|
|
89
|
-
matches.push(`${relativePath}:${i + 1}:${line}`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return matches;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function collectMarkdownArtifacts(projectRoot: string): string[] {
|
|
96
|
-
const frameworkDir = getFrameworkDir(projectRoot);
|
|
97
|
-
const searchPaths = [
|
|
98
|
-
path.join(projectRoot, "docs"),
|
|
99
|
-
path.join(projectRoot, frameworkDir, "docs")
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
const results: string[] = [];
|
|
103
|
-
for (const docsRoot of searchPaths) {
|
|
104
|
-
if (fs.existsSync(docsRoot)) {
|
|
105
|
-
results.push(...collectFilesRecursively(docsRoot, new Set(["md"])).map((filePath) => path.relative(projectRoot, filePath)));
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return Array.from(new Set(results));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function replaceSectionContent(markdown: string, sectionTitle: string, newBody: string): string {
|
|
112
|
-
const escaped = sectionTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
113
|
-
const sectionRegex = new RegExp(`## ${escaped}[\\s\\S]*?(?=\\n## |$)`);
|
|
114
|
-
if (!sectionRegex.test(markdown)) {
|
|
115
|
-
throw new Error(`Section not found: ${sectionTitle}`);
|
|
116
|
-
}
|
|
117
|
-
return markdown.replace(sectionRegex, `## ${sectionTitle}\n\n${newBody.trim()}\n`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function prependToSection(markdown: string, sectionTitle: string, contentToPrepend: string): string {
|
|
121
|
-
const escaped = sectionTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
122
|
-
const sectionRegex = new RegExp(`(## ${escaped}\\n)([\\s\\S]*?)(?=\\n## |$)`);
|
|
123
|
-
const match = markdown.match(sectionRegex);
|
|
124
|
-
if (!match) {
|
|
125
|
-
throw new Error(`Section not found: ${sectionTitle}`);
|
|
126
|
-
}
|
|
127
|
-
const currentBody = match[2].trimStart();
|
|
128
|
-
const updatedBody = `${contentToPrepend.trim()}\n\n${currentBody}`.trim();
|
|
129
|
-
return markdown.replace(sectionRegex, `$1\n${updatedBody}\n`);
|
|
130
|
-
}
|