@vodailoc/kilo-kit-mcp 1.1.1 → 1.2.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.
@@ -0,0 +1,222 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createNoopOrchestrationAudit } from "./orchestration-audit.js";
3
+ import { selectQuestionTemplate } from "./question-templates.js";
4
+ import { routeIntent } from "./router.js";
5
+ export function createOrchestrator(options) {
6
+ const sessions = new Map();
7
+ const audit = options.audit ?? createNoopOrchestrationAudit();
8
+ return {
9
+ orchestrate(input) {
10
+ const session = getOrCreateSession(sessions, options.registry, input);
11
+ mergeInput(session, input);
12
+ const missingInfo = missingRequiredQuestionIds(session.questions, session.answers);
13
+ const suggestions = ensureMemorySuggestions(options.memory, session);
14
+ const pendingSuggestions = suggestions.filter((suggestion) => session.memoryConfirmations[suggestion.key] === undefined);
15
+ const acceptedSuggestions = suggestions.filter((suggestion) => session.memoryConfirmations[suggestion.key] === "accepted");
16
+ const state = selectState(session, missingInfo, pendingSuggestions);
17
+ for (const [suggestionKey, decision] of Object.entries(input.memoryConfirmations ?? {})) {
18
+ options.memory.recordDecision({ suggestionKey, decision });
19
+ }
20
+ const verificationGate = buildVerificationGate(acceptedSuggestions);
21
+ const finalWorkflow = state === "ready" ? session.workflow : undefined;
22
+ const firstSkillToLoad = finalWorkflow?.[0]?.skill;
23
+ const nextAction = buildNextAction(state, session, missingInfo, pendingSuggestions, firstSkillToLoad);
24
+ persistSession(options.memory, session, state, verificationGate, finalWorkflow);
25
+ const auditRef = audit.record({
26
+ sessionId: session.sessionId,
27
+ state,
28
+ taskMode: session.route.taskMode,
29
+ message: session.message,
30
+ nextAction,
31
+ });
32
+ return {
33
+ sessionId: session.sessionId,
34
+ state,
35
+ message: session.message,
36
+ taskMode: session.route.taskMode,
37
+ questions: session.questions,
38
+ missingInfo,
39
+ route: session.route,
40
+ workflow: session.workflow,
41
+ memorySuggestions: suggestions,
42
+ ...(finalWorkflow ? { finalWorkflow } : {}),
43
+ ...(firstSkillToLoad ? { firstSkillToLoad } : {}),
44
+ verificationGate,
45
+ nextAction,
46
+ ...(auditRef ? { auditRef } : {}),
47
+ };
48
+ },
49
+ };
50
+ }
51
+ function getOrCreateSession(sessions, registry, input) {
52
+ if (input.sessionId && sessions.has(input.sessionId)) {
53
+ return sessions.get(input.sessionId);
54
+ }
55
+ const route = routeIntent(registry, {
56
+ message: input.message,
57
+ ...(input.context
58
+ ? {
59
+ context: {
60
+ ...(input.context.files ? { files: input.context.files } : {}),
61
+ ...(input.context.mode ? { mode: input.context.mode } : {}),
62
+ ...(input.context.previousErrors ? { previousErrors: input.context.previousErrors } : {}),
63
+ },
64
+ }
65
+ : {}),
66
+ limit: 5,
67
+ });
68
+ const workflow = ensureBrainstormingFirst(registry, route.workflow, input);
69
+ const template = selectQuestionTemplate({
70
+ taskMode: route.taskMode,
71
+ workflowSkillIds: workflow.map((step) => step.skill.id),
72
+ recommendedSkillIds: route.recommended.map((item) => item.skill.id),
73
+ });
74
+ const session = {
75
+ sessionId: input.sessionId ?? randomUUID(),
76
+ message: input.message,
77
+ createdAt: new Date().toISOString(),
78
+ route,
79
+ workflow,
80
+ questions: template.questions,
81
+ answers: {},
82
+ memorySuggestions: [],
83
+ memoryConfirmations: {},
84
+ ...(input.context ? { context: input.context } : {}),
85
+ };
86
+ sessions.set(session.sessionId, session);
87
+ return session;
88
+ }
89
+ function persistSession(memory, session, state, verificationGate, finalWorkflow) {
90
+ const now = new Date().toISOString();
91
+ memory.recordSession({
92
+ id: session.sessionId,
93
+ state,
94
+ message: session.message,
95
+ taskMode: session.route.taskMode,
96
+ route: toJsonObject(session.route),
97
+ questions: toJsonArray(session.questions),
98
+ answers: { ...session.answers },
99
+ memorySuggestions: session.memorySuggestions.map((suggestion) => ({ ...suggestion })),
100
+ finalWorkflow: toJsonArray(finalWorkflow ?? []),
101
+ createdAt: session.createdAt,
102
+ updatedAt: now,
103
+ });
104
+ if (state === "ready" && finalWorkflow) {
105
+ memory.recordOutcome({
106
+ id: randomUUID(),
107
+ sessionId: session.sessionId,
108
+ taskMode: session.route.taskMode,
109
+ workflow: toJsonArray(finalWorkflow),
110
+ verification: verificationGate,
111
+ outcome: "workflow-released",
112
+ createdAt: now,
113
+ });
114
+ }
115
+ }
116
+ function mergeInput(session, input) {
117
+ session.message = input.message || session.message;
118
+ session.answers = { ...session.answers, ...(input.answers ?? {}) };
119
+ session.memoryConfirmations = { ...session.memoryConfirmations, ...(input.memoryConfirmations ?? {}) };
120
+ if (input.context) {
121
+ session.context = input.context;
122
+ }
123
+ }
124
+ function ensureMemorySuggestions(memory, session) {
125
+ if (session.memorySuggestions.length > 0) {
126
+ return session.memorySuggestions;
127
+ }
128
+ session.memorySuggestions = memory.suggest({
129
+ taskMode: session.route.taskMode,
130
+ workflowSkillIds: session.workflow.map((step) => step.skill.id),
131
+ ...(session.context?.projectFingerprint ? { projectFingerprint: session.context.projectFingerprint } : {}),
132
+ });
133
+ return session.memorySuggestions;
134
+ }
135
+ function selectState(session, missingInfo, pendingSuggestions) {
136
+ if (isSubstantiveWork(session) && Object.keys(session.answers).length === 0) {
137
+ return "brainstorming_required";
138
+ }
139
+ if (missingInfo.length > 0) {
140
+ return "questioning";
141
+ }
142
+ if (pendingSuggestions.length > 0) {
143
+ return "awaiting_memory_confirmation";
144
+ }
145
+ return "ready";
146
+ }
147
+ function ensureBrainstormingFirst(registry, workflow, input) {
148
+ const existing = workflow.filter((step) => step.skill.id !== "productivity/brainstorming");
149
+ const brainstorming = findSkillById(registry, "productivity/brainstorming");
150
+ if (!brainstorming) {
151
+ return workflow;
152
+ }
153
+ const step = {
154
+ skill: brainstorming,
155
+ role: "prepare",
156
+ reason: "C4 Brainstorming-First Gate requires design clarification before substantive work.",
157
+ };
158
+ if (input.context?.mode === "brainstorming" || /\bbrainstorm(?:ing)?\b/i.test(input.message)) {
159
+ return [step, ...existing];
160
+ }
161
+ if (isReadOnlyRequest(input.message)) {
162
+ return workflow;
163
+ }
164
+ return [step, ...existing];
165
+ }
166
+ function findSkillById(registry, id) {
167
+ const [category, skill] = id.split("/");
168
+ if (!category || !skill) {
169
+ return undefined;
170
+ }
171
+ try {
172
+ return registry.getSkill(category, skill);
173
+ }
174
+ catch {
175
+ return undefined;
176
+ }
177
+ }
178
+ function missingRequiredQuestionIds(questions, answers) {
179
+ return questions
180
+ .filter((question) => question.required)
181
+ .filter((question) => !answers[question.id]?.trim())
182
+ .map((question) => question.id);
183
+ }
184
+ function buildVerificationGate(acceptedSuggestions) {
185
+ const commands = acceptedSuggestions.flatMap((suggestion) => {
186
+ const commands = suggestion.value.commands;
187
+ return Array.isArray(commands) ? commands.filter((command) => typeof command === "string") : [];
188
+ });
189
+ return {
190
+ commands: commands.length > 0 ? [...new Set(commands)] : ["npm --prefix mcp test", "npm --prefix mcp run typecheck"],
191
+ reason: commands.length > 0
192
+ ? "Memory-confirmed verification commands must pass before completion."
193
+ : "Default MCP verification gate for C4 orchestration.",
194
+ };
195
+ }
196
+ function buildNextAction(state, session, missingInfo, pendingSuggestions, firstSkillToLoad) {
197
+ if (state === "brainstorming_required") {
198
+ return `Start with productivity/brainstorming and answer required C4 questions: ${missingInfo.join(", ")}.`;
199
+ }
200
+ if (state === "questioning") {
201
+ return `Answer missing C4 questions before workflow execution: ${missingInfo.join(", ")}.`;
202
+ }
203
+ if (state === "awaiting_memory_confirmation") {
204
+ return `Accept or reject memory suggestions before execution: ${pendingSuggestions.map((item) => item.key).join(", ")}.`;
205
+ }
206
+ if (firstSkillToLoad) {
207
+ return `Load ${firstSkillToLoad.id} with kilo_get_skill, then follow the final workflow.`;
208
+ }
209
+ return session.route.nextAction;
210
+ }
211
+ function isSubstantiveWork(session) {
212
+ return !isReadOnlyRequest(session.message);
213
+ }
214
+ function isReadOnlyRequest(message) {
215
+ return /\b(status|show|read|explain|what is|what's|la sao|là sao)\b/i.test(message);
216
+ }
217
+ function toJsonObject(value) {
218
+ return JSON.parse(JSON.stringify(value));
219
+ }
220
+ function toJsonArray(value) {
221
+ return JSON.parse(JSON.stringify(value));
222
+ }
@@ -0,0 +1,249 @@
1
+ const BASE_QUESTIONS = [
2
+ {
3
+ id: "goal",
4
+ prompt: "What exact outcome should this task produce?",
5
+ kind: "text",
6
+ required: true,
7
+ },
8
+ {
9
+ id: "scope",
10
+ prompt: "Which files, modules, or behavior are in scope, and what is explicitly out of scope?",
11
+ kind: "text",
12
+ required: true,
13
+ },
14
+ {
15
+ id: "success_criteria",
16
+ prompt: "What evidence will prove the task is complete and working correctly?",
17
+ kind: "text",
18
+ required: true,
19
+ },
20
+ ];
21
+ const MODE_QUESTIONS = {
22
+ "bug-test-first": {
23
+ id: "engineering:bug-test-first",
24
+ label: "Engineering bug/TDD clarification",
25
+ questions: [
26
+ {
27
+ id: "failing_behavior",
28
+ prompt: "What is the current failing behavior, including the smallest reproducible trigger?",
29
+ kind: "text",
30
+ required: true,
31
+ category: "engineering",
32
+ },
33
+ {
34
+ id: "test_command",
35
+ prompt: "Which focused test or command should fail before the fix and pass after it?",
36
+ kind: "text",
37
+ required: true,
38
+ category: "engineering",
39
+ skillId: "engineering/tdd",
40
+ },
41
+ ],
42
+ },
43
+ ui: {
44
+ id: "design:ui",
45
+ label: "Design and UI clarification",
46
+ questions: [
47
+ {
48
+ id: "audience",
49
+ prompt: "Who will use this interface, and what is their most common workflow?",
50
+ kind: "text",
51
+ required: true,
52
+ category: "design",
53
+ },
54
+ {
55
+ id: "visual_constraints",
56
+ prompt: "Which existing design system, layout, or responsive constraints must be preserved?",
57
+ kind: "text",
58
+ required: true,
59
+ category: "design",
60
+ },
61
+ ],
62
+ },
63
+ review: {
64
+ id: "engineering:review",
65
+ label: "Code review clarification",
66
+ questions: [
67
+ {
68
+ id: "review_target",
69
+ prompt: "Which branch, diff, or files should be reviewed, and what risks matter most?",
70
+ kind: "text",
71
+ required: true,
72
+ category: "engineering",
73
+ skillId: "engineering/code-review",
74
+ },
75
+ {
76
+ id: "merge_gate",
77
+ prompt: "What checks must pass before this can be considered merge-ready?",
78
+ kind: "text",
79
+ required: true,
80
+ category: "engineering",
81
+ },
82
+ ],
83
+ },
84
+ "workflow-optimization": {
85
+ id: "engineering:workflow-optimization",
86
+ label: "Architecture and workflow clarification",
87
+ questions: [
88
+ {
89
+ id: "current_friction",
90
+ prompt: "Where is the current rule or workflow friction showing up in real use?",
91
+ kind: "text",
92
+ required: true,
93
+ category: "engineering",
94
+ },
95
+ {
96
+ id: "desired_operating_model",
97
+ prompt: "What should the agent do differently after this workflow change lands?",
98
+ kind: "text",
99
+ required: true,
100
+ category: "engineering",
101
+ },
102
+ ],
103
+ },
104
+ };
105
+ const CATEGORY_QUESTIONS = {
106
+ engineering: {
107
+ id: "engineering:general",
108
+ label: "Engineering clarification",
109
+ questions: [
110
+ {
111
+ id: "runtime",
112
+ prompt: "Which runtime, framework, or command environment must this work support?",
113
+ kind: "text",
114
+ required: true,
115
+ category: "engineering",
116
+ },
117
+ ],
118
+ },
119
+ design: MODE_QUESTIONS.ui,
120
+ operations: {
121
+ id: "operations:general",
122
+ label: "Operations clarification",
123
+ questions: [
124
+ {
125
+ id: "environment",
126
+ prompt: "Which environment is affected, and what rollback or safety constraint applies?",
127
+ kind: "text",
128
+ required: true,
129
+ category: "operations",
130
+ },
131
+ ],
132
+ },
133
+ "writing-docs": {
134
+ id: "writing-docs:general",
135
+ label: "Documentation clarification",
136
+ questions: [
137
+ {
138
+ id: "audience",
139
+ prompt: "Who is the target reader, and what should they be able to do after reading?",
140
+ kind: "text",
141
+ required: true,
142
+ category: "writing-docs",
143
+ },
144
+ ],
145
+ },
146
+ security: {
147
+ id: "security:general",
148
+ label: "Security clarification",
149
+ questions: [
150
+ {
151
+ id: "risk_boundary",
152
+ prompt: "What asset, permission boundary, or threat scenario must be protected?",
153
+ kind: "text",
154
+ required: true,
155
+ category: "security",
156
+ },
157
+ ],
158
+ },
159
+ fallback: {
160
+ id: "general:fallback",
161
+ label: "General clarification",
162
+ questions: [
163
+ {
164
+ id: "constraints",
165
+ prompt: "What constraints, deadlines, or preferences should shape this workflow?",
166
+ kind: "text",
167
+ required: true,
168
+ },
169
+ ],
170
+ },
171
+ };
172
+ const SKILL_OVERRIDES = {
173
+ "engineering/tdd": [
174
+ {
175
+ id: "test_command",
176
+ prompt: "Which exact test command should demonstrate red before green?",
177
+ kind: "text",
178
+ required: true,
179
+ category: "engineering",
180
+ skillId: "engineering/tdd",
181
+ },
182
+ ],
183
+ "design/frontend-design": [
184
+ {
185
+ id: "visual_constraints",
186
+ prompt: "Which visual style, density, and responsive breakpoints should guide the UI?",
187
+ kind: "text",
188
+ required: true,
189
+ category: "design",
190
+ skillId: "design/frontend-design",
191
+ },
192
+ ],
193
+ "operations/deployment-procedures": [
194
+ {
195
+ id: "rollback_plan",
196
+ prompt: "What rollback signal and rollback command should be ready before deployment?",
197
+ kind: "text",
198
+ required: true,
199
+ category: "operations",
200
+ skillId: "operations/deployment-procedures",
201
+ },
202
+ ],
203
+ "engineering/vulnerability-scanner": [
204
+ {
205
+ id: "risk_boundary",
206
+ prompt: "Which threat model, asset, or exploit class should this security review prioritize?",
207
+ kind: "text",
208
+ required: true,
209
+ category: "engineering",
210
+ skillId: "engineering/vulnerability-scanner",
211
+ },
212
+ ],
213
+ "productivity/verification-before-completion": [
214
+ {
215
+ id: "verification_commands",
216
+ prompt: "Which commands or manual checks must be fresh before completion is claimed?",
217
+ kind: "text",
218
+ required: false,
219
+ category: "productivity",
220
+ skillId: "productivity/verification-before-completion",
221
+ },
222
+ ],
223
+ };
224
+ export function selectQuestionTemplate(input) {
225
+ const modeTemplate = MODE_QUESTIONS[input.taskMode];
226
+ const categoryTemplate = modeTemplate ?? categoryTemplateFor(input);
227
+ const workflowSkillIds = input.workflowSkillIds.length > 0 ? input.workflowSkillIds : input.recommendedSkillIds;
228
+ const overrideQuestions = workflowSkillIds.flatMap((skillId) => SKILL_OVERRIDES[skillId] ?? []);
229
+ return {
230
+ id: categoryTemplate.id,
231
+ label: categoryTemplate.label,
232
+ questions: dedupeQuestions([...BASE_QUESTIONS, ...categoryTemplate.questions, ...overrideQuestions]).slice(0, 8),
233
+ };
234
+ }
235
+ function categoryTemplateFor(input) {
236
+ const firstSkillId = input.workflowSkillIds[0] ?? input.recommendedSkillIds[0];
237
+ const category = firstSkillId?.split("/")[0];
238
+ if (category && CATEGORY_QUESTIONS[category]) {
239
+ return CATEGORY_QUESTIONS[category];
240
+ }
241
+ return CATEGORY_QUESTIONS.fallback;
242
+ }
243
+ function dedupeQuestions(questions) {
244
+ const byId = new Map();
245
+ for (const question of questions) {
246
+ byId.set(question.id, question);
247
+ }
248
+ return [...byId.values()];
249
+ }
@@ -0,0 +1,149 @@
1
+ import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ export function createInMemoryRouteAnalytics(initialEvents = []) {
4
+ const events = [...initialEvents];
5
+ return {
6
+ record(event) {
7
+ events.push(cloneEvent(event));
8
+ },
9
+ events() {
10
+ return events.map(cloneEvent);
11
+ },
12
+ report() {
13
+ return buildReport(events);
14
+ },
15
+ scoreAdjustment(skillId, taskMode) {
16
+ return scoreAdjustment(events, skillId, taskMode);
17
+ },
18
+ };
19
+ }
20
+ export function createJsonlRouteAnalytics(options) {
21
+ return {
22
+ record(event) {
23
+ mkdirSync(path.dirname(options.filePath), { recursive: true });
24
+ appendFileSync(options.filePath, `${JSON.stringify(event)}\n`, "utf8");
25
+ },
26
+ events() {
27
+ return readJsonlEvents(options.filePath);
28
+ },
29
+ report() {
30
+ return buildReport(readJsonlEvents(options.filePath));
31
+ },
32
+ scoreAdjustment(skillId, taskMode) {
33
+ return scoreAdjustment(readJsonlEvents(options.filePath), skillId, taskMode);
34
+ },
35
+ };
36
+ }
37
+ function buildReport(events) {
38
+ const taskModes = new Map();
39
+ const workflows = new Map();
40
+ const skills = new Map();
41
+ const conflicts = new Map();
42
+ for (const event of events) {
43
+ taskModes.set(event.taskMode, (taskModes.get(event.taskMode) ?? 0) + 1);
44
+ const workflowKey = event.workflowSkillIds.join(" -> ");
45
+ if (workflowKey) {
46
+ const current = workflows.get(workflowKey) ?? { workflow: event.workflowSkillIds, count: 0 };
47
+ current.count += 1;
48
+ workflows.set(workflowKey, current);
49
+ }
50
+ event.recommendedSkillIds.forEach((skillId) => {
51
+ const stats = skillStats(skills, skillId);
52
+ stats.timesRecommended += 1;
53
+ });
54
+ event.workflowSkillIds.forEach((skillId, index) => {
55
+ const stats = skillStats(skills, skillId);
56
+ stats.timesInWorkflow += 1;
57
+ if (index === 0) {
58
+ stats.timesPrimary += 1;
59
+ }
60
+ });
61
+ for (const entry of event.decisionTrail) {
62
+ const stats = skillStats(skills, entry.skillId);
63
+ stats.totalScore += entry.score;
64
+ stats.scoreSamples += 1;
65
+ const conflict = entry.scoreBreakdown.conflict ?? 0;
66
+ if (conflict < 0) {
67
+ const current = conflicts.get(entry.skillId) ?? { count: 0, totalPenalty: 0 };
68
+ current.count += 1;
69
+ current.totalPenalty += conflict;
70
+ conflicts.set(entry.skillId, current);
71
+ }
72
+ }
73
+ }
74
+ return {
75
+ totalEvents: events.length,
76
+ taskModes: [...taskModes.entries()]
77
+ .map(([taskMode, count]) => ({ taskMode, count }))
78
+ .sort((left, right) => right.count - left.count || left.taskMode.localeCompare(right.taskMode)),
79
+ workflows: [...workflows.values()].sort((left, right) => right.count - left.count || left.workflow.join(">").localeCompare(right.workflow.join(">"))),
80
+ topSkills: [...skills.entries()]
81
+ .map(([skillId, stats]) => ({
82
+ skillId,
83
+ timesRecommended: stats.timesRecommended,
84
+ timesInWorkflow: stats.timesInWorkflow,
85
+ timesPrimary: stats.timesPrimary,
86
+ avgScore: stats.scoreSamples === 0 ? 0 : Math.round((stats.totalScore / stats.scoreSamples) * 100) / 100,
87
+ }))
88
+ .sort((left, right) => right.timesPrimary - left.timesPrimary ||
89
+ right.timesRecommended + right.timesInWorkflow - (left.timesRecommended + left.timesInWorkflow) ||
90
+ right.avgScore - left.avgScore ||
91
+ left.skillId.localeCompare(right.skillId)),
92
+ conflictPenalties: [...conflicts.entries()]
93
+ .map(([skillId, stats]) => ({ skillId, count: stats.count, totalPenalty: stats.totalPenalty }))
94
+ .sort((left, right) => right.count - left.count || left.skillId.localeCompare(right.skillId)),
95
+ };
96
+ }
97
+ function scoreAdjustment(events, skillId, taskMode) {
98
+ const sameMode = events.filter((event) => event.taskMode === taskMode);
99
+ const recommended = sameMode.filter((event) => event.recommendedSkillIds.includes(skillId)).length;
100
+ const inWorkflow = sameMode.filter((event) => event.workflowSkillIds.includes(skillId)).length;
101
+ const primary = sameMode.filter((event) => event.workflowSkillIds[0] === skillId).length;
102
+ return Math.min(12, recommended * 3 + inWorkflow * 2 + primary * 3);
103
+ }
104
+ function skillStats(skills, skillId) {
105
+ const existing = skills.get(skillId);
106
+ if (existing) {
107
+ return existing;
108
+ }
109
+ const created = { timesRecommended: 0, timesInWorkflow: 0, timesPrimary: 0, totalScore: 0, scoreSamples: 0 };
110
+ skills.set(skillId, created);
111
+ return created;
112
+ }
113
+ function readJsonlEvents(filePath) {
114
+ let content = "";
115
+ try {
116
+ content = readFileSync(filePath, "utf8");
117
+ }
118
+ catch (error) {
119
+ if (error.code === "ENOENT") {
120
+ return [];
121
+ }
122
+ throw error;
123
+ }
124
+ return content
125
+ .split(/\r?\n/)
126
+ .filter(Boolean)
127
+ .map((line) => parseRouteEvent(line))
128
+ .filter((event) => event !== null);
129
+ }
130
+ function parseRouteEvent(line) {
131
+ try {
132
+ const parsed = JSON.parse(line);
133
+ if (typeof parsed.timestamp !== "string" ||
134
+ typeof parsed.message !== "string" ||
135
+ typeof parsed.taskMode !== "string" ||
136
+ !Array.isArray(parsed.recommendedSkillIds) ||
137
+ !Array.isArray(parsed.workflowSkillIds) ||
138
+ !Array.isArray(parsed.decisionTrail)) {
139
+ return null;
140
+ }
141
+ return parsed;
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ }
147
+ function cloneEvent(event) {
148
+ return JSON.parse(JSON.stringify(event));
149
+ }