muonroi-cli 1.8.0 → 1.8.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/LICENSE +21 -21
- package/README.md +122 -122
- package/dist/src/agent-harness/mock-model.js +112 -4
- package/dist/src/cli/cost-forensics.js +17 -14
- package/dist/src/council/prompts.js +20 -20
- package/dist/src/flow/compaction/index.d.ts +1 -0
- package/dist/src/flow/compaction/index.js +4 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/index.js +4 -2
- package/dist/src/lsp/manager.js +11 -3
- package/dist/src/lsp/manager.test.js +39 -0
- package/dist/src/mcp/auto-setup.js +0 -8
- package/dist/src/mcp/oauth-callback.js +2 -2
- package/dist/src/mcp/parse-headers.test.js +14 -14
- package/dist/src/mcp/self-verify-jobs.d.ts +1 -0
- package/dist/src/mcp/self-verify-runner.js +1 -1
- package/dist/src/mcp/setup-guide-text.js +74 -74
- package/dist/src/mcp/smoke.test.js +43 -43
- package/dist/src/ops/doctor.js +7 -7
- package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +1 -0
- package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
- package/dist/src/orchestrator/__tests__/prompts.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/prompts.test.js +88 -0
- package/dist/src/orchestrator/__tests__/sub-session-delegation.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/sub-session-delegation.test.js +507 -0
- package/dist/src/orchestrator/auto-commit.js +9 -7
- package/dist/src/orchestrator/message-processor.d.ts +2 -0
- package/dist/src/orchestrator/message-processor.js +7 -0
- package/dist/src/orchestrator/orchestrator.d.ts +3 -0
- package/dist/src/orchestrator/orchestrator.js +362 -28
- package/dist/src/orchestrator/pending-calls.js +2 -1
- package/dist/src/orchestrator/preprocessor.js +3 -2
- package/dist/src/orchestrator/prompts.d.ts +8 -0
- package/dist/src/orchestrator/prompts.js +65 -18
- package/dist/src/orchestrator/sub-agent-cap.d.ts +2 -0
- package/dist/src/orchestrator/sub-agent-cap.js +12 -4
- package/dist/src/orchestrator/subagent-compactor.d.ts +7 -0
- package/dist/src/orchestrator/subagent-compactor.js +65 -14
- package/dist/src/orchestrator/subagent-compactor.spec.js +125 -9
- package/dist/src/orchestrator/tool-engine.d.ts +2 -0
- package/dist/src/orchestrator/tool-engine.js +80 -21
- package/dist/src/orchestrator/turn-runner-deps.d.ts +2 -0
- package/dist/src/pil/__tests__/llm-classify.test.js +79 -1
- package/dist/src/pil/layer1-intent.js +37 -37
- package/dist/src/pil/layer2_5-ponytail.js +8 -8
- package/dist/src/pil/llm-classify.d.ts +11 -0
- package/dist/src/pil/llm-classify.js +193 -0
- package/dist/src/product-loop/done-gate.js +3 -3
- package/dist/src/product-loop/loop-driver.js +18 -18
- package/dist/src/product-loop/progress-snapshot.js +4 -4
- package/dist/src/providers/mcp-vision-bridge.js +48 -48
- package/dist/src/reporter/index.js +1 -1
- package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
- package/dist/src/scaffold/bb-quality-gate.js +5 -5
- package/dist/src/scaffold/continuation-prompt.js +60 -60
- package/dist/src/scaffold/init-new.js +453 -453
- package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
- package/dist/src/self-qa/agentic-loop.js +11 -2
- package/dist/src/storage/__tests__/hierarchical-session.test.d.ts +1 -0
- package/dist/src/storage/__tests__/hierarchical-session.test.js +161 -0
- package/dist/src/storage/__tests__/migrations.test.js +2 -2
- package/dist/src/storage/__tests__/sweep-stale-pending.test.js +2 -2
- package/dist/src/storage/index.d.ts +1 -1
- package/dist/src/storage/index.js +1 -1
- package/dist/src/storage/interaction-log.js +5 -5
- package/dist/src/storage/migrations.js +135 -123
- package/dist/src/storage/session-experience-store.js +4 -4
- package/dist/src/storage/sessions.js +43 -43
- package/dist/src/storage/transcript-response-entry.test.js +18 -5
- package/dist/src/storage/transcript.d.ts +2 -0
- package/dist/src/storage/transcript.js +223 -97
- package/dist/src/storage/usage.js +34 -17
- package/dist/src/storage/workspaces.js +12 -12
- package/dist/src/tools/native-tools.js +10 -1
- package/dist/src/tools/registry.d.ts +1 -0
- package/dist/src/tools/registry.js +30 -2
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/slash/council-inspect.js +4 -4
- package/dist/src/ui/slash/expand.js +14 -1
- package/dist/src/ui/use-app-logic.js +0 -0
- package/dist/src/utils/__tests__/compaction-caps.test.js +9 -9
- package/dist/src/utils/__tests__/llm-deadline.test.js +16 -4
- package/dist/src/utils/__tests__/logger.test.d.ts +1 -0
- package/dist/src/utils/__tests__/logger.test.js +115 -0
- package/dist/src/utils/__tests__/settings-agent-first.test.d.ts +1 -0
- package/dist/src/utils/__tests__/settings-agent-first.test.js +48 -0
- package/dist/src/utils/clipboard-image.js +23 -23
- package/dist/src/utils/install-manager.d.ts +1 -0
- package/dist/src/utils/install-manager.js +76 -7
- package/dist/src/utils/install-manager.test.js +43 -1
- package/dist/src/utils/logger.d.ts +28 -0
- package/dist/src/utils/logger.js +171 -0
- package/dist/src/utils/settings.d.ts +9 -1
- package/dist/src/utils/settings.js +59 -22
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/utils/skills.js +3 -3
- package/dist/src/utils/update-checker.test.js +5 -1
- package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* DeepSeek Flash). Timeout 2500ms — bails fast if the model stalls.
|
|
14
14
|
*/
|
|
15
15
|
import { streamText } from "ai";
|
|
16
|
+
import { getModelByTier, getModelInfo } from "../models/registry.js";
|
|
16
17
|
import { getProviderCapabilities } from "../providers/capabilities.js";
|
|
17
18
|
import { resolveModelRuntime } from "../providers/runtime.js";
|
|
18
19
|
const LLM_CLASSIFY_TIMEOUT_MS = 2500;
|
|
@@ -271,4 +272,196 @@ export function createLlmClassifier(factory, modelId) {
|
|
|
271
272
|
}
|
|
272
273
|
};
|
|
273
274
|
}
|
|
275
|
+
export function classifySubSessionActionHeuristic(prompt) {
|
|
276
|
+
const trimmed = prompt.trim().toLowerCase();
|
|
277
|
+
if (!trimmed)
|
|
278
|
+
return null;
|
|
279
|
+
// Strip trailing punctuation for list-based matching so "hello!" and "cảm ơn!"
|
|
280
|
+
// still hit the static lists instead of falling through to the LLM classifier.
|
|
281
|
+
const stripped = trimmed.replace(/[!?.…,;:]+$/g, "").trim();
|
|
282
|
+
// 1. Simple math equations (exact matches like "2+2", "1 + 1")
|
|
283
|
+
if (/^\d+\s*[+\-*/]\s*\d+$/.test(trimmed)) {
|
|
284
|
+
return {
|
|
285
|
+
action: "DIRECT_ANSWER",
|
|
286
|
+
confidence: 0.99,
|
|
287
|
+
reason: "Obvious input classified via heuristic (simple math)",
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
// 2. Greetings (exact matches only, trailing punctuation stripped)
|
|
291
|
+
const greetings = [
|
|
292
|
+
"hi",
|
|
293
|
+
"hello",
|
|
294
|
+
"hey",
|
|
295
|
+
"chào",
|
|
296
|
+
"xin chào",
|
|
297
|
+
"hi there",
|
|
298
|
+
"hello there",
|
|
299
|
+
"chào bạn",
|
|
300
|
+
"halo",
|
|
301
|
+
"hola",
|
|
302
|
+
"bạn ơi",
|
|
303
|
+
];
|
|
304
|
+
if (greetings.includes(stripped)) {
|
|
305
|
+
return {
|
|
306
|
+
action: "DIRECT_ANSWER",
|
|
307
|
+
confidence: 0.99,
|
|
308
|
+
reason: "Obvious input classified via heuristic (greeting)",
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// 3. Thanks (exact matches only, trailing punctuation stripped)
|
|
312
|
+
const thanks = [
|
|
313
|
+
"thanks",
|
|
314
|
+
"thank you",
|
|
315
|
+
"cảm ơn",
|
|
316
|
+
"cám ơn",
|
|
317
|
+
"thank",
|
|
318
|
+
"thx",
|
|
319
|
+
"ty",
|
|
320
|
+
"cảm ơn bạn",
|
|
321
|
+
"cám ơn bạn",
|
|
322
|
+
"cảm ơn nhé",
|
|
323
|
+
"cám ơn nhé",
|
|
324
|
+
];
|
|
325
|
+
if (thanks.includes(stripped)) {
|
|
326
|
+
return {
|
|
327
|
+
action: "DIRECT_ANSWER",
|
|
328
|
+
confidence: 0.99,
|
|
329
|
+
reason: "Obvious input classified via heuristic (thanks)",
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
// 4. Help (exact matches only, trailing punctuation stripped)
|
|
333
|
+
const help = ["help", "hướng dẫn", "cứu", "help me"];
|
|
334
|
+
if (help.includes(stripped)) {
|
|
335
|
+
return {
|
|
336
|
+
action: "DIRECT_ANSWER",
|
|
337
|
+
confidence: 0.99,
|
|
338
|
+
reason: "Obvious input classified via heuristic (help)",
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
// 5. Short conversational words / acknowledgements (exact matches only, trailing punctuation stripped)
|
|
342
|
+
const conversation = [
|
|
343
|
+
"ok",
|
|
344
|
+
"okay",
|
|
345
|
+
"yes",
|
|
346
|
+
"no",
|
|
347
|
+
"vâng",
|
|
348
|
+
"dạ",
|
|
349
|
+
"ừ",
|
|
350
|
+
"chắc thế",
|
|
351
|
+
"ừm",
|
|
352
|
+
"umm",
|
|
353
|
+
"cool",
|
|
354
|
+
"nice",
|
|
355
|
+
"perfect",
|
|
356
|
+
"done",
|
|
357
|
+
"xong",
|
|
358
|
+
"yep",
|
|
359
|
+
"yup",
|
|
360
|
+
"nah",
|
|
361
|
+
"fine",
|
|
362
|
+
"tốt",
|
|
363
|
+
"được",
|
|
364
|
+
"okie",
|
|
365
|
+
];
|
|
366
|
+
if (conversation.includes(stripped)) {
|
|
367
|
+
return {
|
|
368
|
+
action: "DIRECT_ANSWER",
|
|
369
|
+
confidence: 0.99,
|
|
370
|
+
reason: "Obvious input classified via heuristic (acknowledgement)",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
const ROUTER_SYSTEM_PROMPT = "You are a routing controller for an AI coding agent. Your goal is to decide the execution strategy for the user's prompt based on the conversation history and metadata.\n\n" +
|
|
376
|
+
"Analyze the user's prompt and select one of the following ACTIONS:\n" +
|
|
377
|
+
'- "DIRECT_ANSWER": The prompt is informational, a quick question, a code review, an explanation, greeting, or thanks. No file creation/modification, test execution, or multi-turn tool runs are needed.\n' +
|
|
378
|
+
'- "ROTATE_SESSION": The user is starting a completely new topic or task unrelated to the active discussion (e.g. "let\'s switch to writing a python script", "forget the previous bug, show me how to..."). OR, if the session size (metadata) exceeds the rotation threshold and the active task is completed or the prompt starts a new focus, choose ROTATE_SESSION to prune/summarize the context.\n' +
|
|
379
|
+
'- "SPAWN_SUB_SESSION": The user wants to execute a multi-step task (e.g. "write tests for X and debug it", "refactor the storage layer", "implement feature Y", "fix all compile errors"). This requires running multiple tools (file edits, bash commands, searches).\n\n' +
|
|
380
|
+
"Response format: Reply with exactly one comma-separated line containing:\n" +
|
|
381
|
+
"<ACTION>,<CONFIDENCE>,<REASON>\n\n" +
|
|
382
|
+
"Examples:\n" +
|
|
383
|
+
'- "DIRECT_ANSWER,0.95,Simple explanation of how the DB migration works."\n' +
|
|
384
|
+
'- "ROTATE_SESSION,0.90,Complete shift to a different project/language."\n' +
|
|
385
|
+
'- "ROTATE_SESSION,0.95,Session size exceeds threshold and current request starts a new task."\n' +
|
|
386
|
+
'- "SPAWN_SUB_SESSION,0.98,Requires writing a test suite and fixing multiple files to get it green."\n' +
|
|
387
|
+
"No other text, only the comma-separated line.";
|
|
388
|
+
export async function classifySubSessionAction(factory, modelId, prompt, contextInfo, signal) {
|
|
389
|
+
if (process.env.MUONROI_DISABLE_HEURISTIC_ROUTING !== "1") {
|
|
390
|
+
const heuristic = classifySubSessionActionHeuristic(prompt);
|
|
391
|
+
if (heuristic)
|
|
392
|
+
return heuristic;
|
|
393
|
+
}
|
|
394
|
+
const controller = new AbortController();
|
|
395
|
+
let timer;
|
|
396
|
+
try {
|
|
397
|
+
// Zero-hardcode: query models catalog for a cheap fast-tier model under the same provider.
|
|
398
|
+
const info = getModelInfo(modelId);
|
|
399
|
+
const provider = info?.provider;
|
|
400
|
+
const fastModel = provider ? getModelByTier("fast", provider) || getModelByTier("balanced", provider) : undefined;
|
|
401
|
+
const classificationModelId = fastModel?.id ?? modelId;
|
|
402
|
+
const runtime = resolveModelRuntime(factory, classificationModelId);
|
|
403
|
+
const isReasoning = runtime.modelInfo?.reasoning === true;
|
|
404
|
+
timer = setTimeout(() => controller.abort(), isReasoning ? REASONING_CLASSIFY_TIMEOUT_MS : LLM_CLASSIFY_TIMEOUT_MS);
|
|
405
|
+
const combinedSignal = signal
|
|
406
|
+
? (AbortSignal.any?.([signal, controller.signal]) ?? controller.signal)
|
|
407
|
+
: controller.signal;
|
|
408
|
+
const dropMaxTokens = runtime.unsupportedParams?.includes("maxOutputTokens") === true;
|
|
409
|
+
const maxOut = isReasoning ? REASONING_MAX_OUTPUT_TOKENS : NONREASONING_MAX_OUTPUT_TOKENS;
|
|
410
|
+
let providerOptions = runtime.providerOptions;
|
|
411
|
+
if (isReasoning && runtime.modelInfo?.supportsReasoningEffort && runtime.modelInfo.provider) {
|
|
412
|
+
const lowEffort = getProviderCapabilities(runtime.modelInfo.provider).buildProviderOptions({
|
|
413
|
+
model: runtime.model,
|
|
414
|
+
reasoningEffort: "low",
|
|
415
|
+
});
|
|
416
|
+
providerOptions = mergeProviderOptions(runtime.providerOptions, lowEffort);
|
|
417
|
+
}
|
|
418
|
+
let promptWithContext = prompt.slice(0, 1000);
|
|
419
|
+
if (contextInfo) {
|
|
420
|
+
promptWithContext =
|
|
421
|
+
`[SESSION METADATA]\n` +
|
|
422
|
+
`Current session size: ${contextInfo.currentChars} characters.\n` +
|
|
423
|
+
`Rotation threshold: ${contextInfo.threshold} characters.\n\n` +
|
|
424
|
+
`[USER PROMPT]\n${promptWithContext}`;
|
|
425
|
+
}
|
|
426
|
+
const result = streamText({
|
|
427
|
+
model: runtime.model,
|
|
428
|
+
abortSignal: combinedSignal,
|
|
429
|
+
system: ROUTER_SYSTEM_PROMPT,
|
|
430
|
+
prompt: promptWithContext,
|
|
431
|
+
...(dropMaxTokens ? {} : { maxOutputTokens: maxOut }),
|
|
432
|
+
...(providerOptions ? { providerOptions } : {}),
|
|
433
|
+
});
|
|
434
|
+
let text = "";
|
|
435
|
+
let reasoningText = "";
|
|
436
|
+
for await (const part of result.fullStream) {
|
|
437
|
+
if (part.type === "text-delta")
|
|
438
|
+
text += part.textDelta ?? part.text ?? "";
|
|
439
|
+
else if (part.type === "reasoning-delta")
|
|
440
|
+
reasoningText += part.textDelta ?? part.text ?? "";
|
|
441
|
+
}
|
|
442
|
+
const rawResult = text.trim() || reasoningText.trim();
|
|
443
|
+
if (!rawResult)
|
|
444
|
+
return null;
|
|
445
|
+
const clean = rawResult.replace(/[`*"]/g, "").trim();
|
|
446
|
+
const firstLine = clean.split(/\r?\n/)[0] ?? "";
|
|
447
|
+
const parts = firstLine.split(",");
|
|
448
|
+
if (parts.length < 2)
|
|
449
|
+
return null;
|
|
450
|
+
const action = parts[0].trim().toUpperCase();
|
|
451
|
+
const confidence = Number(parts[1].trim()) || 0.8;
|
|
452
|
+
const reason = parts.slice(2).join(",").trim() || "No reason given";
|
|
453
|
+
if (action === "DIRECT_ANSWER" || action === "ROTATE_SESSION" || action === "SPAWN_SUB_SESSION") {
|
|
454
|
+
return { action, confidence, reason };
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
console.error(`[pil.llm-classify] classifySubSessionAction failed: ${err?.message}`);
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
finally {
|
|
463
|
+
if (timer)
|
|
464
|
+
clearTimeout(timer);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
274
467
|
//# sourceMappingURL=llm-classify.js.map
|
|
@@ -142,9 +142,9 @@ async function runCustomerDebate(ctx) {
|
|
|
142
142
|
const criteriaText = ctx.criteria
|
|
143
143
|
.map((c) => `- ${c.id}: ${c.status}${c.evidence ? ` (Evidence: ${c.evidence})` : ""}`)
|
|
144
144
|
.join("\n");
|
|
145
|
-
let conversation = `System: You are in a "Definition of Done" debate.
|
|
146
|
-
PO's goal: Prove the product is ready to ship.
|
|
147
|
-
Customer's goal: Ensure all requirements are met and it's high quality.
|
|
145
|
+
let conversation = `System: You are in a "Definition of Done" debate.
|
|
146
|
+
PO's goal: Prove the product is ready to ship.
|
|
147
|
+
Customer's goal: Ensure all requirements are met and it's high quality.
|
|
148
148
|
Criteria:\n${criteriaText}\n`;
|
|
149
149
|
for (let r = 1; r <= rounds; r++) {
|
|
150
150
|
const poPrompt = `${conversation}\nRound ${r}: PO, explain why this is ready to ship.`;
|
|
@@ -684,24 +684,24 @@ export async function* runLoopDriver(ctx) {
|
|
|
684
684
|
},
|
|
685
685
|
};
|
|
686
686
|
// Synthesize ProductSpec
|
|
687
|
-
const synthesisPrompt = `Synthesize a ProductSpec JSON based on the following:
|
|
688
|
-
Idea: ${ctx.idea}
|
|
689
|
-
Clarified Spec: ${JSON.stringify(clarifiedSpec)}
|
|
690
|
-
Debate Summary: ${debateState.runningSummary}
|
|
691
|
-
Research Findings: ${debateState.researchFindings ?? "N/A"}
|
|
692
|
-
|
|
693
|
-
Output ONLY a JSON object matching this interface:
|
|
694
|
-
interface ProductSpec {
|
|
695
|
-
idea: string;
|
|
696
|
-
persona: string;
|
|
697
|
-
mvp: string[];
|
|
698
|
-
phase2: string[];
|
|
699
|
-
architecture: string;
|
|
700
|
-
ioContract: string;
|
|
701
|
-
folderStructure: string;
|
|
702
|
-
sprintEstimate: number;
|
|
703
|
-
costEstimate: number;
|
|
704
|
-
}
|
|
687
|
+
const synthesisPrompt = `Synthesize a ProductSpec JSON based on the following:
|
|
688
|
+
Idea: ${ctx.idea}
|
|
689
|
+
Clarified Spec: ${JSON.stringify(clarifiedSpec)}
|
|
690
|
+
Debate Summary: ${debateState.runningSummary}
|
|
691
|
+
Research Findings: ${debateState.researchFindings ?? "N/A"}
|
|
692
|
+
|
|
693
|
+
Output ONLY a JSON object matching this interface:
|
|
694
|
+
interface ProductSpec {
|
|
695
|
+
idea: string;
|
|
696
|
+
persona: string;
|
|
697
|
+
mvp: string[];
|
|
698
|
+
phase2: string[];
|
|
699
|
+
architecture: string;
|
|
700
|
+
ioContract: string;
|
|
701
|
+
folderStructure: string;
|
|
702
|
+
sprintEstimate: number;
|
|
703
|
+
costEstimate: number;
|
|
704
|
+
}
|
|
705
705
|
`;
|
|
706
706
|
// The scoping phase's only LLM call. Wrapped so a provider hang/
|
|
707
707
|
// timeout leaves a council_error audit row instead of swallowing the
|
|
@@ -20,10 +20,10 @@ function readLatestSprintStage(runId) {
|
|
|
20
20
|
try {
|
|
21
21
|
const db = getDatabase();
|
|
22
22
|
const row = db
|
|
23
|
-
.prepare(`SELECT metadata_json, created_at
|
|
24
|
-
FROM interaction_logs
|
|
25
|
-
WHERE session_id = ? AND event_type = 'ui_interaction' AND event_subtype = 'sprint_stage'
|
|
26
|
-
ORDER BY id DESC
|
|
23
|
+
.prepare(`SELECT metadata_json, created_at
|
|
24
|
+
FROM interaction_logs
|
|
25
|
+
WHERE session_id = ? AND event_type = 'ui_interaction' AND event_subtype = 'sprint_stage'
|
|
26
|
+
ORDER BY id DESC
|
|
27
27
|
LIMIT 1`)
|
|
28
28
|
.get(runId);
|
|
29
29
|
if (!row)
|
|
@@ -48,27 +48,27 @@ const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bm
|
|
|
48
48
|
* Downstream agents consume this JSON directly instead of re-parsing markdown.
|
|
49
49
|
* Kept embedded in the prompt (not strict JSON-schema mode) for cross-provider portability.
|
|
50
50
|
*/
|
|
51
|
-
export const UI_LAYOUT_SCHEMA_HINT = `{
|
|
52
|
-
"viewport": { "width": number, "height": number, "deviceClass": "mobile" | "tablet" | "desktop" },
|
|
53
|
-
"tokens": {
|
|
54
|
-
"colors": [{ "name": string, "hex": string, "role": "primary" | "secondary" | "accent" | "bg" | "fg" | "border" | "muted" }],
|
|
55
|
-
"typography": [{ "role": "heading" | "body" | "caption" | "label", "fontFamily": string, "sizePx": number, "weight": number }],
|
|
56
|
-
"spacingScalePx": number[],
|
|
57
|
-
"radiusScalePx": number[]
|
|
58
|
-
},
|
|
59
|
-
"layout": { "type": "stack" | "grid" | "flex" | "absolute", "direction": "row" | "column", "gapPx": number, "columns": number | null },
|
|
60
|
-
"components": [{
|
|
61
|
-
"id": string,
|
|
62
|
-
"role": "button" | "input" | "card" | "nav" | "header" | "footer" | "image" | "icon" | "text" | "list" | "modal" | "tabs" | "table" | "chart" | "container" | "other",
|
|
63
|
-
"label": string,
|
|
64
|
-
"text": string | null,
|
|
65
|
-
"bbox": { "x": number, "y": number, "w": number, "h": number },
|
|
66
|
-
"style": { "bgHex": string | null, "fgHex": string | null, "borderHex": string | null, "radiusPx": number | null, "shadow": string | null },
|
|
67
|
-
"state": "default" | "hover" | "active" | "disabled" | "focus" | "error",
|
|
68
|
-
"children": string[]
|
|
69
|
-
}],
|
|
70
|
-
"hierarchy": [{ "level": 1 | 2 | 3, "text": string, "componentId": string }],
|
|
71
|
-
"notes": string[]
|
|
51
|
+
export const UI_LAYOUT_SCHEMA_HINT = `{
|
|
52
|
+
"viewport": { "width": number, "height": number, "deviceClass": "mobile" | "tablet" | "desktop" },
|
|
53
|
+
"tokens": {
|
|
54
|
+
"colors": [{ "name": string, "hex": string, "role": "primary" | "secondary" | "accent" | "bg" | "fg" | "border" | "muted" }],
|
|
55
|
+
"typography": [{ "role": "heading" | "body" | "caption" | "label", "fontFamily": string, "sizePx": number, "weight": number }],
|
|
56
|
+
"spacingScalePx": number[],
|
|
57
|
+
"radiusScalePx": number[]
|
|
58
|
+
},
|
|
59
|
+
"layout": { "type": "stack" | "grid" | "flex" | "absolute", "direction": "row" | "column", "gapPx": number, "columns": number | null },
|
|
60
|
+
"components": [{
|
|
61
|
+
"id": string,
|
|
62
|
+
"role": "button" | "input" | "card" | "nav" | "header" | "footer" | "image" | "icon" | "text" | "list" | "modal" | "tabs" | "table" | "chart" | "container" | "other",
|
|
63
|
+
"label": string,
|
|
64
|
+
"text": string | null,
|
|
65
|
+
"bbox": { "x": number, "y": number, "w": number, "h": number },
|
|
66
|
+
"style": { "bgHex": string | null, "fgHex": string | null, "borderHex": string | null, "radiusPx": number | null, "shadow": string | null },
|
|
67
|
+
"state": "default" | "hover" | "active" | "disabled" | "focus" | "error",
|
|
68
|
+
"children": string[]
|
|
69
|
+
}],
|
|
70
|
+
"hierarchy": [{ "level": 1 | 2 | 3, "text": string, "componentId": string }],
|
|
71
|
+
"notes": string[]
|
|
72
72
|
}`;
|
|
73
73
|
const MIME_MAP = {
|
|
74
74
|
".png": "image/png",
|
|
@@ -319,33 +319,33 @@ export async function askVisionProxy(question, imageIdOrPath, cwd, signal) {
|
|
|
319
319
|
export function getVisionGuidanceForTextOnly(modelId) {
|
|
320
320
|
if (!needsVisionProxy(modelId))
|
|
321
321
|
return "";
|
|
322
|
-
return `
|
|
323
|
-
VISION PROXY (text-only model):
|
|
324
|
-
You cannot see images directly. You have vision proxy tools to work with ANY image:
|
|
325
|
-
|
|
326
|
-
TOOLS:
|
|
327
|
-
- analyze_image: Proactively analyze an image from a file path, URL, or base64. Use this FIRST when you encounter or need to work with an image.
|
|
328
|
-
- ask_vision_proxy: Ask a specific follow-up question about any cached image (or provide a file path for a new image).
|
|
329
|
-
- list_vision_cache: See all cached images available for querying.
|
|
330
|
-
|
|
331
|
-
WHEN TO USE (be PROACTIVE, not passive):
|
|
332
|
-
- User mentions an image file → analyze_image immediately
|
|
333
|
-
- User pastes or references a screenshot → it's auto-analyzed, use ask_vision_proxy for details
|
|
334
|
-
- Working with UI/web pages → prefer browser_snapshot (text-based), use screenshot + ask_vision_proxy when visual details matter
|
|
335
|
-
- Reviewing design mockups, diagrams, charts → analyze_image the file
|
|
336
|
-
- Debugging visual issues (CSS, layout, colors) → take screenshot, then ask_vision_proxy specific questions
|
|
337
|
-
- Comparing before/after → analyze both images, ask about differences
|
|
338
|
-
- Reading text from images (OCR) → analyze_image with a question like "transcribe all text"
|
|
339
|
-
- Any file with image extension (.png, .jpg, .gif, .webp, .svg, etc.) → analyze_image
|
|
340
|
-
|
|
341
|
-
WORKFLOW:
|
|
342
|
-
1. Encounter image → analyze_image (or it's auto-analyzed from tool results)
|
|
343
|
-
2. Need more detail → ask_vision_proxy with specific question
|
|
344
|
-
3. Need to verify changes → take new screenshot/analyze new image → compare
|
|
345
|
-
|
|
346
|
-
Images are cached (up to ${IMAGE_CACHE_MAX}, ${IMAGE_CACHE_TTL_MS / 60000}min TTL). You can reference them by ID for follow-ups.
|
|
347
|
-
|
|
348
|
-
IMPORTANT: Do NOT guess what an image contains. Always use the proxy to get accurate information.
|
|
322
|
+
return `
|
|
323
|
+
VISION PROXY (text-only model):
|
|
324
|
+
You cannot see images directly. You have vision proxy tools to work with ANY image:
|
|
325
|
+
|
|
326
|
+
TOOLS:
|
|
327
|
+
- analyze_image: Proactively analyze an image from a file path, URL, or base64. Use this FIRST when you encounter or need to work with an image.
|
|
328
|
+
- ask_vision_proxy: Ask a specific follow-up question about any cached image (or provide a file path for a new image).
|
|
329
|
+
- list_vision_cache: See all cached images available for querying.
|
|
330
|
+
|
|
331
|
+
WHEN TO USE (be PROACTIVE, not passive):
|
|
332
|
+
- User mentions an image file → analyze_image immediately
|
|
333
|
+
- User pastes or references a screenshot → it's auto-analyzed, use ask_vision_proxy for details
|
|
334
|
+
- Working with UI/web pages → prefer browser_snapshot (text-based), use screenshot + ask_vision_proxy when visual details matter
|
|
335
|
+
- Reviewing design mockups, diagrams, charts → analyze_image the file
|
|
336
|
+
- Debugging visual issues (CSS, layout, colors) → take screenshot, then ask_vision_proxy specific questions
|
|
337
|
+
- Comparing before/after → analyze both images, ask about differences
|
|
338
|
+
- Reading text from images (OCR) → analyze_image with a question like "transcribe all text"
|
|
339
|
+
- Any file with image extension (.png, .jpg, .gif, .webp, .svg, etc.) → analyze_image
|
|
340
|
+
|
|
341
|
+
WORKFLOW:
|
|
342
|
+
1. Encounter image → analyze_image (or it's auto-analyzed from tool results)
|
|
343
|
+
2. Need more detail → ask_vision_proxy with specific question
|
|
344
|
+
3. Need to verify changes → take new screenshot/analyze new image → compare
|
|
345
|
+
|
|
346
|
+
Images are cached (up to ${IMAGE_CACHE_MAX}, ${IMAGE_CACHE_TTL_MS / 60000}min TTL). You can reference them by ID for follow-ups.
|
|
347
|
+
|
|
348
|
+
IMPORTANT: Do NOT guess what an image contains. Always use the proxy to get accurate information.
|
|
349
349
|
`;
|
|
350
350
|
}
|
|
351
351
|
// Keep backward compat export
|
|
@@ -28,7 +28,7 @@ const HEARTBEAT_EVERY_N_POLLS = 12; // ~60s at default 5s poll interval
|
|
|
28
28
|
function emitHeartbeat(runId) {
|
|
29
29
|
try {
|
|
30
30
|
const db = getDatabase();
|
|
31
|
-
db.prepare(`INSERT INTO interaction_logs (session_id, event_type, event_subtype, metadata_json, created_at)
|
|
31
|
+
db.prepare(`INSERT INTO interaction_logs (session_id, event_type, event_subtype, metadata_json, created_at)
|
|
32
32
|
VALUES (?, 'reporter', 'heartbeat', '{}', ?)`).run(runId, new Date().toISOString());
|
|
33
33
|
}
|
|
34
34
|
catch {
|
|
@@ -91,24 +91,24 @@ function buildSampleRuleCs(projectName, intent) {
|
|
|
91
91
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
92
92
|
.join("");
|
|
93
93
|
const contextTypeName = `${projectName}RuleContext`;
|
|
94
|
-
return `using Muonroi.RuleEngine.Abstractions;
|
|
95
|
-
using Muonroi.RuleEngine.Core;
|
|
96
|
-
|
|
97
|
-
namespace ${projectName}.Domain.Rules;
|
|
98
|
-
|
|
99
|
-
/// <summary>
|
|
100
|
-
/// Sample rule for intent: ${intent}.
|
|
101
|
-
/// Generated by muonroi-cli — replace stub logic with real business logic.
|
|
102
|
-
/// </summary>
|
|
103
|
-
[MExtractAsRule("SAMPLE_${intent.toUpperCase().replace(/[-\s]/g, "_")}", DependsOn = new string[] { })]
|
|
104
|
-
public class Sample${intentPascal}Rule : IRule<${contextTypeName}>
|
|
105
|
-
{
|
|
106
|
-
public Task<RuleResult> EvaluateAsync(${contextTypeName} context, CancellationToken cancellationToken = default)
|
|
107
|
-
{
|
|
108
|
-
// TODO: Replace with real ${intent} evaluation logic.
|
|
109
|
-
return Task.FromResult(RuleResult.Passed());
|
|
110
|
-
}
|
|
111
|
-
}
|
|
94
|
+
return `using Muonroi.RuleEngine.Abstractions;
|
|
95
|
+
using Muonroi.RuleEngine.Core;
|
|
96
|
+
|
|
97
|
+
namespace ${projectName}.Domain.Rules;
|
|
98
|
+
|
|
99
|
+
/// <summary>
|
|
100
|
+
/// Sample rule for intent: ${intent}.
|
|
101
|
+
/// Generated by muonroi-cli — replace stub logic with real business logic.
|
|
102
|
+
/// </summary>
|
|
103
|
+
[MExtractAsRule("SAMPLE_${intent.toUpperCase().replace(/[-\s]/g, "_")}", DependsOn = new string[] { })]
|
|
104
|
+
public class Sample${intentPascal}Rule : IRule<${contextTypeName}>
|
|
105
|
+
{
|
|
106
|
+
public Task<RuleResult> EvaluateAsync(${contextTypeName} context, CancellationToken cancellationToken = default)
|
|
107
|
+
{
|
|
108
|
+
// TODO: Replace with real ${intent} evaluation logic.
|
|
109
|
+
return Task.FromResult(RuleResult.Passed());
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
112
|
`;
|
|
113
113
|
}
|
|
114
114
|
export async function generateSampleRule(serverDir, projectName, intent, fsOps) {
|
|
@@ -133,32 +133,32 @@ function buildSampleRuleTestCs(projectName, intent) {
|
|
|
133
133
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
134
134
|
.join("");
|
|
135
135
|
const contextTypeName = `${projectName}RuleContext`;
|
|
136
|
-
return `using Muonroi.RuleEngine.Abstractions;
|
|
137
|
-
using Muonroi.RuleEngine.Core;
|
|
138
|
-
using Xunit;
|
|
139
|
-
|
|
140
|
-
namespace ${projectName}.UnitTests.Rules;
|
|
141
|
-
|
|
142
|
-
/// <summary>
|
|
143
|
-
/// Tests for Sample${intentPascal}Rule.
|
|
144
|
-
/// Generated by muonroi-cli — extend with real test cases.
|
|
145
|
-
/// </summary>
|
|
146
|
-
public class Sample${intentPascal}RuleTests
|
|
147
|
-
{
|
|
148
|
-
[Fact]
|
|
149
|
-
public async Task EvaluateAsync_ReturnsRuleResultPassed()
|
|
150
|
-
{
|
|
151
|
-
// Arrange — senior-grade: uses FactBag + reflection-free context construction
|
|
152
|
-
var context = new ${contextTypeName}();
|
|
153
|
-
var rule = new Sample${intentPascal}Rule();
|
|
154
|
-
|
|
155
|
-
// Act
|
|
156
|
-
var result = await rule.EvaluateAsync(context, CancellationToken.None);
|
|
157
|
-
|
|
158
|
-
// Assert
|
|
159
|
-
Assert.Equal(RuleResultStatus.Passed, result.Status);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
136
|
+
return `using Muonroi.RuleEngine.Abstractions;
|
|
137
|
+
using Muonroi.RuleEngine.Core;
|
|
138
|
+
using Xunit;
|
|
139
|
+
|
|
140
|
+
namespace ${projectName}.UnitTests.Rules;
|
|
141
|
+
|
|
142
|
+
/// <summary>
|
|
143
|
+
/// Tests for Sample${intentPascal}Rule.
|
|
144
|
+
/// Generated by muonroi-cli — extend with real test cases.
|
|
145
|
+
/// </summary>
|
|
146
|
+
public class Sample${intentPascal}RuleTests
|
|
147
|
+
{
|
|
148
|
+
[Fact]
|
|
149
|
+
public async Task EvaluateAsync_ReturnsRuleResultPassed()
|
|
150
|
+
{
|
|
151
|
+
// Arrange — senior-grade: uses FactBag + reflection-free context construction
|
|
152
|
+
var context = new ${contextTypeName}();
|
|
153
|
+
var rule = new Sample${intentPascal}Rule();
|
|
154
|
+
|
|
155
|
+
// Act
|
|
156
|
+
var result = await rule.EvaluateAsync(context, CancellationToken.None);
|
|
157
|
+
|
|
158
|
+
// Assert
|
|
159
|
+
Assert.Equal(RuleResultStatus.Passed, result.Status);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
162
|
`;
|
|
163
163
|
}
|
|
164
164
|
export async function generateSampleRuleTest(serverDir, projectName, intent, fsOps) {
|
|
@@ -246,9 +246,9 @@ export async function copyBoundaryScript(serverDir, bbRepoRoot, fsOps) {
|
|
|
246
246
|
const ciContent = fsOps.readFile(ciYmlPath);
|
|
247
247
|
// Idempotency: skip if already wired
|
|
248
248
|
if (!ciContent.includes(BOUNDARIES_SCRIPT_NAME)) {
|
|
249
|
-
const boundaryStep = `
|
|
250
|
-
- name: Check modular boundaries
|
|
251
|
-
run: pwsh ./scripts/${BOUNDARIES_SCRIPT_NAME} -RepoRoot .
|
|
249
|
+
const boundaryStep = `
|
|
250
|
+
- name: Check modular boundaries
|
|
251
|
+
run: pwsh ./scripts/${BOUNDARIES_SCRIPT_NAME} -RepoRoot .
|
|
252
252
|
`;
|
|
253
253
|
// Append before the last line or after the last job step
|
|
254
254
|
const updated = `${ciContent.trimEnd()}\n${boundaryStep}\n`;
|
|
@@ -93,11 +93,11 @@ export async function runQualityGateWithRetry(opts, onRetryCodeGen) {
|
|
|
93
93
|
const failureSummary = firstResult.failures
|
|
94
94
|
.map((f) => `### ${f.step}\n\`\`\`\n${f.output.slice(0, 500)}\n\`\`\``)
|
|
95
95
|
.join("\n\n");
|
|
96
|
-
const councilPrompt = `${opts.intentPrompt}
|
|
97
|
-
|
|
98
|
-
## Gate failures (please fix in next iteration)
|
|
99
|
-
${failureSummary}
|
|
100
|
-
|
|
96
|
+
const councilPrompt = `${opts.intentPrompt}
|
|
97
|
+
|
|
98
|
+
## Gate failures (please fix in next iteration)
|
|
99
|
+
${failureSummary}
|
|
100
|
+
|
|
101
101
|
Please revise the BB ecosystem wiring to fix these compilation/boundary errors.`;
|
|
102
102
|
// Call council once with failures appended
|
|
103
103
|
await continueAsCouncil({
|