muonroi-cli 1.4.1 → 1.5.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.
Files changed (172) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
  4. package/dist/src/agent-harness/__tests__/mock-model.spec.js +48 -1
  5. package/dist/src/agent-harness/mock-model.d.ts +11 -0
  6. package/dist/src/agent-harness/mock-model.js +21 -0
  7. package/dist/src/cli/cost-forensics.js +12 -12
  8. package/dist/src/council/__tests__/clarification-prompt.test.js +51 -0
  9. package/dist/src/council/__tests__/clarifier-ready-gate.test.js +32 -0
  10. package/dist/src/council/__tests__/decisions-lock.test.js +17 -1
  11. package/dist/src/council/__tests__/oauth-reachable.test.d.ts +1 -0
  12. package/dist/src/council/__tests__/oauth-reachable.test.js +31 -0
  13. package/dist/src/council/__tests__/parse-outcome-fallback.test.js +11 -0
  14. package/dist/src/council/clarifier.js +9 -1
  15. package/dist/src/council/debate.js +5 -1
  16. package/dist/src/council/decisions-lock.js +3 -3
  17. package/dist/src/council/index.js +12 -5
  18. package/dist/src/council/leader.d.ts +0 -17
  19. package/dist/src/council/leader.js +22 -15
  20. package/dist/src/council/planner.js +1 -1
  21. package/dist/src/council/prompts.js +63 -57
  22. package/dist/src/council/types.d.ts +7 -0
  23. package/dist/src/ee/__tests__/ee-onboarding.test.d.ts +1 -0
  24. package/dist/src/ee/__tests__/ee-onboarding.test.js +32 -0
  25. package/dist/src/ee/auth.d.ts +9 -0
  26. package/dist/src/ee/auth.js +19 -0
  27. package/dist/src/ee/ee-onboarding.d.ts +5 -0
  28. package/dist/src/ee/ee-onboarding.js +76 -0
  29. package/dist/src/generated/version.d.ts +1 -1
  30. package/dist/src/generated/version.js +1 -1
  31. package/dist/src/headless/output.js +6 -4
  32. package/dist/src/headless/output.test.js +4 -3
  33. package/dist/src/index.js +20 -1
  34. package/dist/src/mcp/__tests__/auto-setup.test.js +74 -0
  35. package/dist/src/mcp/__tests__/client-pool.spec.d.ts +1 -0
  36. package/dist/src/mcp/__tests__/client-pool.spec.js +98 -0
  37. package/dist/src/mcp/__tests__/parallel-build.spec.d.ts +1 -0
  38. package/dist/src/mcp/__tests__/parallel-build.spec.js +67 -0
  39. package/dist/src/mcp/__tests__/smart-filter.test.js +56 -0
  40. package/dist/src/mcp/auto-setup.js +56 -2
  41. package/dist/src/mcp/client-pool.d.ts +46 -0
  42. package/dist/src/mcp/client-pool.js +212 -0
  43. package/dist/src/mcp/oauth-callback.js +2 -2
  44. package/dist/src/mcp/parse-headers.test.js +14 -14
  45. package/dist/src/mcp/runtime.d.ts +28 -0
  46. package/dist/src/mcp/runtime.js +117 -51
  47. package/dist/src/mcp/self-verify-runner.d.ts +14 -0
  48. package/dist/src/mcp/self-verify-runner.js +38 -0
  49. package/dist/src/mcp/setup-guide-text.d.ts +9 -0
  50. package/dist/src/mcp/setup-guide-text.js +84 -0
  51. package/dist/src/mcp/smart-filter.js +49 -0
  52. package/dist/src/mcp/smoke.test.js +43 -43
  53. package/dist/src/mcp/tools-server.d.ts +7 -0
  54. package/dist/src/mcp/tools-server.js +19 -22
  55. package/dist/src/models/catalog.json +349 -349
  56. package/dist/src/ops/__tests__/doctor-ee-health.test.js +21 -0
  57. package/dist/src/ops/doctor.d.ts +3 -2
  58. package/dist/src/ops/doctor.js +47 -11
  59. package/dist/src/ops/doctor.test.js +4 -3
  60. package/dist/src/orchestrator/__tests__/mcp-capability-block.test.d.ts +1 -0
  61. package/dist/src/orchestrator/__tests__/mcp-capability-block.test.js +39 -0
  62. package/dist/src/orchestrator/__tests__/project-stack.test.d.ts +1 -0
  63. package/dist/src/orchestrator/__tests__/project-stack.test.js +65 -0
  64. package/dist/src/orchestrator/batch-turn-runner.js +7 -11
  65. package/dist/src/orchestrator/message-processor.js +57 -27
  66. package/dist/src/orchestrator/orchestrator.js +26 -0
  67. package/dist/src/orchestrator/prompts.d.ts +51 -0
  68. package/dist/src/orchestrator/prompts.js +257 -134
  69. package/dist/src/orchestrator/scope-ceiling.js +6 -1
  70. package/dist/src/orchestrator/stream-runner.js +20 -15
  71. package/dist/src/orchestrator/text-tool-call-detector.test.js +13 -13
  72. package/dist/src/pil/__tests__/clarity-gate.test.js +24 -215
  73. package/dist/src/pil/__tests__/config.test.js +1 -17
  74. package/dist/src/pil/__tests__/discovery.test.js +144 -11
  75. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +7 -2
  76. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  77. package/dist/src/pil/__tests__/layer16-clarity.test.js +32 -116
  78. package/dist/src/pil/__tests__/layer4-gsd.test.js +37 -0
  79. package/dist/src/pil/__tests__/layer6-output.test.js +137 -18
  80. package/dist/src/pil/__tests__/llm-classify.test.js +49 -2
  81. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  82. package/dist/src/pil/agent-operating-contract.js +2 -0
  83. package/dist/src/pil/agent-operating-contract.test.js +7 -2
  84. package/dist/src/pil/cheap-model-playbook.js +35 -35
  85. package/dist/src/pil/cheap-model-workbooks.js +16 -13
  86. package/dist/src/pil/clarity-gate.d.ts +21 -19
  87. package/dist/src/pil/clarity-gate.js +26 -153
  88. package/dist/src/pil/config.d.ts +9 -1
  89. package/dist/src/pil/config.js +15 -4
  90. package/dist/src/pil/discovery.js +211 -136
  91. package/dist/src/pil/layer1-intent.d.ts +12 -0
  92. package/dist/src/pil/layer1-intent.js +283 -38
  93. package/dist/src/pil/layer1-intent.test.js +210 -4
  94. package/dist/src/pil/layer16-clarity.d.ts +25 -11
  95. package/dist/src/pil/layer16-clarity.js +19 -306
  96. package/dist/src/pil/layer4-gsd.js +18 -6
  97. package/dist/src/pil/layer6-output.d.ts +2 -0
  98. package/dist/src/pil/layer6-output.js +137 -22
  99. package/dist/src/pil/llm-classify.d.ts +26 -0
  100. package/dist/src/pil/llm-classify.js +34 -5
  101. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  102. package/dist/src/pil/native-capabilities-workbook.js +82 -76
  103. package/dist/src/pil/schema.d.ts +8 -0
  104. package/dist/src/pil/schema.js +12 -1
  105. package/dist/src/pil/task-tier-map.js +4 -0
  106. package/dist/src/pil/types.d.ts +11 -1
  107. package/dist/src/product-loop/done-gate.js +3 -3
  108. package/dist/src/product-loop/loop-driver.js +18 -18
  109. package/dist/src/product-loop/progress-snapshot.js +4 -4
  110. package/dist/src/providers/auth/gemini-oauth.js +6 -15
  111. package/dist/src/providers/auth/grok-oauth.js +6 -15
  112. package/dist/src/providers/auth/openai-oauth.js +6 -15
  113. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  114. package/dist/src/reporter/index.js +1 -1
  115. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  116. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  117. package/dist/src/scaffold/continuation-prompt.js +60 -60
  118. package/dist/src/scaffold/init-new.js +453 -453
  119. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  120. package/dist/src/self-qa/agentic-loop.js +24 -19
  121. package/dist/src/self-qa/spec-emitter.js +26 -23
  122. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  123. package/dist/src/storage/interaction-log.js +5 -5
  124. package/dist/src/storage/migrations.js +122 -122
  125. package/dist/src/storage/sessions.js +42 -42
  126. package/dist/src/storage/transcript.js +91 -84
  127. package/dist/src/storage/usage.js +14 -14
  128. package/dist/src/storage/workspaces.js +12 -12
  129. package/dist/src/tools/__tests__/native-tools.test.d.ts +1 -0
  130. package/dist/src/tools/__tests__/native-tools.test.js +53 -0
  131. package/dist/src/tools/git-safety.d.ts +61 -0
  132. package/dist/src/tools/git-safety.js +141 -0
  133. package/dist/src/tools/git-safety.test.d.ts +1 -0
  134. package/dist/src/tools/git-safety.test.js +111 -0
  135. package/dist/src/tools/native-tools.d.ts +31 -0
  136. package/dist/src/tools/native-tools.js +273 -0
  137. package/dist/src/tools/registry-git-safety.test.d.ts +7 -0
  138. package/dist/src/tools/registry-git-safety.test.js +92 -0
  139. package/dist/src/tools/registry.js +39 -4
  140. package/dist/src/ui/__tests__/markdown-render.test.d.ts +1 -0
  141. package/dist/src/ui/__tests__/markdown-render.test.js +48 -0
  142. package/dist/src/ui/app.js +0 -0
  143. package/dist/src/ui/components/message-view.js +4 -1
  144. package/dist/src/ui/components/structured-response-view.js +7 -3
  145. package/dist/src/ui/components/tool-group.js +7 -1
  146. package/dist/src/ui/markdown-render.d.ts +41 -0
  147. package/dist/src/ui/markdown-render.js +223 -0
  148. package/dist/src/ui/markdown.d.ts +10 -0
  149. package/dist/src/ui/markdown.js +12 -35
  150. package/dist/src/ui/slash/council-inspect.js +4 -4
  151. package/dist/src/ui/slash/export.js +4 -4
  152. package/dist/src/ui/utils/text.d.ts +8 -0
  153. package/dist/src/ui/utils/text.js +16 -0
  154. package/dist/src/ui/utils/text.test.d.ts +1 -0
  155. package/dist/src/ui/utils/text.test.js +23 -0
  156. package/dist/src/usage/ledger.js +48 -15
  157. package/dist/src/utils/__tests__/footprint-gitignore.test.d.ts +1 -0
  158. package/dist/src/utils/__tests__/footprint-gitignore.test.js +50 -0
  159. package/dist/src/utils/clipboard-image.js +23 -23
  160. package/dist/src/utils/open-url.d.ts +56 -0
  161. package/dist/src/utils/open-url.js +58 -0
  162. package/dist/src/utils/open-url.test.d.ts +1 -0
  163. package/dist/src/utils/open-url.test.js +86 -0
  164. package/dist/src/utils/settings.d.ts +12 -0
  165. package/dist/src/utils/settings.js +48 -0
  166. package/dist/src/utils/side-question.js +2 -2
  167. package/dist/src/utils/skills.js +3 -3
  168. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  169. package/dist/src/verify/environment.js +2 -1
  170. package/package.json +1 -1
  171. package/dist/src/pil/layer16-clarity.test.js +0 -31
  172. /package/dist/src/{pil/layer16-clarity.test.d.ts → council/__tests__/clarification-prompt.test.d.ts} +0 -0
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseInline } from "../markdown-render.js";
3
+ import { dark } from "../theme.js";
4
+ const t = dark;
5
+ const text = (segs) => segs.map((s) => s.text).join("");
6
+ describe("parseInline — marker concealment", () => {
7
+ it("strips bold markers and styles the inner text", () => {
8
+ const segs = parseInline("a **bold** b", t);
9
+ expect(text(segs)).toBe("a bold b");
10
+ const bold = segs.find((s) => s.text === "bold");
11
+ expect(bold?.bold).toBe(true);
12
+ expect(bold?.fg).toBe(t.mdBold);
13
+ });
14
+ it("strips italic markers (* and _)", () => {
15
+ expect(text(parseInline("an *italic* word", t))).toBe("an italic word");
16
+ expect(text(parseInline("an _italic_ word", t))).toBe("an italic word");
17
+ expect(parseInline("an *italic* word", t).find((s) => s.text === "italic")?.italic).toBe(true);
18
+ });
19
+ it("handles bold+italic ***x***", () => {
20
+ const seg = parseInline("***wow***", t).find((s) => s.text === "wow");
21
+ expect(seg?.bold).toBe(true);
22
+ expect(seg?.italic).toBe(true);
23
+ });
24
+ it("strips inline code backticks and colors it", () => {
25
+ const segs = parseInline("call `.catch(next)` here", t);
26
+ expect(text(segs)).toBe("call .catch(next) here");
27
+ expect(segs.find((s) => s.text === ".catch(next)")?.fg).toBe(t.mdCode);
28
+ });
29
+ it("renders link label only, dropping the url", () => {
30
+ const segs = parseInline("see [the docs](https://x/y) now", t);
31
+ expect(text(segs)).toBe("see the docs now");
32
+ expect(text(segs)).not.toContain("https://x/y");
33
+ expect(segs.find((s) => s.text === "the docs")?.underline).toBe(true);
34
+ });
35
+ it("strips strikethrough ~~x~~", () => {
36
+ expect(text(parseInline("~~gone~~", t))).toBe("gone");
37
+ });
38
+ it("leaves unterminated markers as literal text (streaming-safe)", () => {
39
+ expect(text(parseInline("a **partial answer", t))).toBe("a **partial answer");
40
+ expect(text(parseInline("trailing `code", t))).toBe("trailing `code");
41
+ });
42
+ it("never leaves ** ` ### markers in styled segments", () => {
43
+ const sample = "**A** and `b` and ***c*** and [d](http://e) and ~~f~~";
44
+ const out = text(parseInline(sample, t));
45
+ expect(out).not.toMatch(/\*\*|`|~~|\]\(/);
46
+ });
47
+ });
48
+ //# sourceMappingURL=markdown-render.test.js.map
Binary file
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/reac
2
2
  import { memo } from "react";
3
3
  import { Markdown } from "../markdown.js";
4
4
  import { PlanView } from "../plan.js";
5
- import { trunc } from "../utils/text.js";
5
+ import { stripStrayModelMacros, trunc } from "../utils/text.js";
6
6
  import { describeMcpFsTool, toolArgs, toolLabel, tryParseArg } from "../utils/tools.js";
7
7
  import { DiffView, ReadFilePreviewView } from "./diff-view.js";
8
8
  import { LspDiagnosticsView, LspResultView } from "./lsp-views.js";
@@ -29,6 +29,9 @@ const USER_MSG_COLLAPSED_LINES = 5;
29
29
  // cost. 8 fits comfortably on a short terminal and conveys the gist.
30
30
  const ASSISTANT_MSG_COLLAPSED_LINES = 8;
31
31
  export function AssistantMessageContent({ content, t, expanded, isFinal, }) {
32
+ // Strip stray model self-annotation macros (e.g. grok's trailing
33
+ // `\confidence{85}`) that leak into the answer — not instructed in the prompt.
34
+ content = stripStrayModelMacros(content);
32
35
  const lines = content.split("\n");
33
36
  const isLong = lines.length > ASSISTANT_MSG_COLLAPSED_LINES;
34
37
  // Phase 5 F7 — the FINAL assistant message in a turn IS the answer the
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
+ import { renderMarkdown } from "../markdown-render.js";
2
3
  export function StructuredResponseView({ t, sr, modeColor }) {
3
4
  const d = sr.data;
4
5
  switch (sr.taskType) {
@@ -27,17 +28,20 @@ export function StructuredResponseView({ t, sr, modeColor }) {
27
28
  }
28
29
  case "documentation": {
29
30
  const r = d;
30
- return (_jsxs("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [_jsx("text", { children: r.content }), (r.examples ?? []).map((ex, i) => (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsx("text", { fg: t.textMuted, children: ex.description }), _jsx("text", { fg: t.mdCode, children: ex.code })] }, `de${i}`)))] }));
31
+ return (_jsxs("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [r.content ? renderMarkdown(r.content, t) : null, (r.examples ?? []).map((ex, i) => (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsx("text", { fg: t.textMuted, children: ex.description }), _jsx("text", { fg: t.mdCode, children: ex.code })] }, `de${i}`)))] }));
31
32
  }
32
33
  case "generate": {
33
34
  const r = d;
34
35
  return (_jsxs("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [r.explanation && _jsx("text", { fg: t.textMuted, children: r.explanation }), (r.files ?? []).map((f, i) => (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsxs("text", { children: [_jsx("span", { style: { fg: t.accent }, children: "── " }), _jsx("span", { style: { fg: t.accent }, children: f.path }), _jsx("span", { style: { fg: t.textMuted }, children: ` (${f.language})` }), _jsx("span", { style: { fg: t.accent }, children: " ──" })] }), _jsx("text", { fg: t.mdCodeBlockFg, children: f.content })] }, `gf${i}`)))] }));
35
36
  }
36
37
  case "general": {
38
+ // `reasoning` is the model's internal justification — deliberately NOT
39
+ // surfaced (it leaked as a "── reasoning:" tail and reads as process
40
+ // narration). The user-facing answer is `response`, rendered as markdown.
37
41
  const g = d;
38
42
  if (!g.response)
39
43
  return _jsx("text", { fg: t.textMuted, children: JSON.stringify(d, null, 2) });
40
- return (_jsxs("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [_jsx("text", { fg: t.text, children: g.response }), g.reasoning ? (_jsxs("text", { fg: t.textMuted, children: [" ── reasoning: ", g.reasoning] })) : null] }));
44
+ return (_jsx("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: renderMarkdown(g.response, t) }));
41
45
  }
42
46
  default: {
43
47
  // Graceful fallback for taskTypes without a dedicated renderer (e.g. a new
@@ -50,7 +54,7 @@ export function StructuredResponseView({ t, sr, modeColor }) {
50
54
  (typeof obj.text === "string" && obj.text) ||
51
55
  null;
52
56
  if (primary) {
53
- return (_jsxs("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [_jsx("text", { fg: t.text, children: primary }), _jsx("text", { fg: t.textMuted, children: ` ── (renderer missing for taskType: ${sr.taskType})` })] }));
57
+ return (_jsxs("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [renderMarkdown(primary, t), _jsx("text", { fg: t.textMuted, children: ` ── (renderer missing for taskType: ${sr.taskType})` })] }));
54
58
  }
55
59
  return _jsx("text", { fg: t.text, children: JSON.stringify(d, null, 2) });
56
60
  }
@@ -15,6 +15,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
15
15
  import { Semantic } from "@muonroi/agent-harness-opentui";
16
16
  import { trunc } from "../utils/text.js";
17
17
  import { dominantVerb, toolLabel } from "../utils/tools.js";
18
+ import { DiffView } from "./diff-view.js";
19
+ // Tools whose result carries a FileDiff worth rendering inline under the item
20
+ // line. Mirrors the per-tool branch in message-view.tsx so grouped edits show
21
+ // the same +/- diff a non-grouped tool_result would.
22
+ const DIFF_TOOLS = new Set(["write_file", "edit_file"]);
18
23
  // Max items rendered inline while a group is active. Anything beyond gets a
19
24
  // "+N more (ctrl+e expand)" affordance — matches Claude Code's overflow line.
20
25
  const ACTIVE_INLINE_LIMIT = 8;
@@ -79,7 +84,8 @@ export function ToolGroupView({ entry, t, expanded, modeColor }) {
79
84
  return (_jsx(Semantic, { id: `tool-group-${g.id}`, role: "region", name: headerVerb, state: g.state, children: _jsxs("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { style: { fg: headerColor }, children: "● " }), _jsx("span", { style: { fg: t.text }, children: headerVerb }), _jsx("span", { style: { fg: t.textMuted }, children: ` (${stats})` })] }), showItems && visibleItems.length > 0 && (_jsxs("box", { paddingLeft: 2, flexDirection: "column", children: [overflow > 0 && (_jsx("text", { fg: t.textMuted, children: `… +${overflow} earlier ${overflow > 1 ? "tools" : "tool"} (ctrl+e to expand)` })), visibleItems.map((it) => {
80
85
  const label = trunc(toolLabel(it.toolCall), 90);
81
86
  const errSuffix = it.failed && it.result?.error ? ` — ${trunc(it.result.error.replace(/\s+/g, " "), 60)}` : "";
82
- return (_jsxs("text", { children: [_jsx("span", { style: { fg: itemColor(it, t) }, children: `${itemGlyph(it)} ` }), _jsx("span", { style: { fg: t.textMuted }, children: label }), errSuffix && _jsx("span", { style: { fg: t.diffRemovedFg }, children: errSuffix })] }, it.toolCall.id));
87
+ const diff = !it.failed && DIFF_TOOLS.has(it.toolCall.function.name) ? it.result?.diff : undefined;
88
+ return (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { style: { fg: itemColor(it, t) }, children: `${itemGlyph(it)} ` }), _jsx("span", { style: { fg: t.textMuted }, children: label }), errSuffix && _jsx("span", { style: { fg: t.diffRemovedFg }, children: errSuffix })] }), diff && _jsx(DiffView, { t: t, diff: diff })] }, it.toolCall.id));
83
89
  })] })), g.state === "done" && !expanded && total > 0 && (_jsx("box", { paddingLeft: 2, children: _jsx("text", { fg: t.textDim, children: "ctrl+e to expand" }) }))] }) }));
84
90
  }
85
91
  //# sourceMappingURL=tool-group.js.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * src/ui/markdown-render.tsx
3
+ *
4
+ * Self-contained markdown → styled-text renderer for the TUI.
5
+ *
6
+ * Why this exists: the bundled `@opentui/core` MarkdownRenderable (v0.1.107)
7
+ * does NOT support concealing syntax markers — the `conceal` prop passed to
8
+ * `<markdown>` is silently ignored, so `**bold**`, `### heading`, `` `code` ``
9
+ * and `- bullet` all render with their literal markers visible (verified via
10
+ * @opentui/react testRender). On top of that the tree-sitter wasm used for
11
+ * highlight often fails to load, leaving raw unstyled text. The result reads
12
+ * like machine output rather than a rendered answer.
13
+ *
14
+ * This renderer parses the common markdown constructs an LLM answer uses and
15
+ * emits opentui `<text>`/`<span>` nodes with theme colors and the markers
16
+ * stripped. It is intentionally pragmatic (not CommonMark-complete): headings,
17
+ * bold, italic, bold+italic, inline code, strikethrough, links, ordered and
18
+ * unordered lists, blockquotes, fenced code blocks, and horizontal rules.
19
+ * Unterminated inline markers degrade to literal text (streaming-safe).
20
+ */
21
+ import type { ReactNode } from "react";
22
+ import type { Theme } from "./theme.js";
23
+ export interface InlineSegment {
24
+ text: string;
25
+ fg?: string;
26
+ bold?: boolean;
27
+ italic?: boolean;
28
+ underline?: boolean;
29
+ strike?: boolean;
30
+ }
31
+ /**
32
+ * Parse a single line of inline markdown into styled segments, with markers
33
+ * removed. `base` carries the inherited style (used when recursing into the
34
+ * body of a bold/italic span so emphasis nests).
35
+ */
36
+ export declare function parseInline(line: string, t: Theme, base?: Partial<InlineSegment>): InlineSegment[];
37
+ /**
38
+ * Render markdown `content` into themed opentui nodes with all syntax markers
39
+ * concealed. Returns a column box; safe to embed inside any flex container.
40
+ */
41
+ export declare function renderMarkdown(content: string, t: Theme): ReactNode;
@@ -0,0 +1,223 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
+ import { detectLang, tokenize } from "./syntax-highlight.js";
3
+ // Inline markers, longest-first so `***` wins over `**`/`*` and `~~` is whole.
4
+ const INLINE = [
5
+ { open: "***", close: "***", bold: true, italic: true },
6
+ { open: "___", close: "___", bold: true, italic: true },
7
+ { open: "**", close: "**", bold: true },
8
+ { open: "__", close: "__", bold: true },
9
+ { open: "~~", close: "~~", strike: true },
10
+ { open: "*", close: "*", italic: true },
11
+ { open: "_", close: "_", italic: true },
12
+ ];
13
+ function pushText(out, text, base) {
14
+ if (!text)
15
+ return;
16
+ const last = out[out.length - 1];
17
+ // Merge adjacent plain segments to keep the span count low.
18
+ if (last && !last.fg && !last.bold && !last.italic && !last.underline && !last.strike && isPlain(base)) {
19
+ last.text += text;
20
+ return;
21
+ }
22
+ out.push({ text, ...base });
23
+ }
24
+ function isPlain(b) {
25
+ return !b.fg && !b.bold && !b.italic && !b.underline && !b.strike;
26
+ }
27
+ /**
28
+ * Parse a single line of inline markdown into styled segments, with markers
29
+ * removed. `base` carries the inherited style (used when recursing into the
30
+ * body of a bold/italic span so emphasis nests).
31
+ */
32
+ export function parseInline(line, t, base = {}) {
33
+ const out = [];
34
+ let i = 0;
35
+ let plainStart = 0;
36
+ const flushPlain = (end) => {
37
+ if (end > plainStart)
38
+ pushText(out, line.slice(plainStart, end), base);
39
+ };
40
+ while (i < line.length) {
41
+ const ch = line[i];
42
+ // Inline code: `code` — highest precedence, no nested formatting inside.
43
+ if (ch === "`") {
44
+ const end = line.indexOf("`", i + 1);
45
+ if (end > i) {
46
+ flushPlain(i);
47
+ out.push({ text: line.slice(i + 1, end), fg: t.mdCode });
48
+ i = end + 1;
49
+ plainStart = i;
50
+ continue;
51
+ }
52
+ }
53
+ // Link: [label](url) — show the label as an underlined link, drop the url.
54
+ if (ch === "[") {
55
+ const close = line.indexOf("]", i + 1);
56
+ if (close > i && line[close + 1] === "(") {
57
+ const urlEnd = line.indexOf(")", close + 2);
58
+ if (urlEnd > close) {
59
+ flushPlain(i);
60
+ const label = line.slice(i + 1, close);
61
+ for (const seg of parseInline(label, t, { ...base, fg: t.mdLinkText, underline: true }))
62
+ out.push(seg);
63
+ i = urlEnd + 1;
64
+ plainStart = i;
65
+ continue;
66
+ }
67
+ }
68
+ }
69
+ // Emphasis markers.
70
+ let matched = false;
71
+ for (const m of INLINE) {
72
+ if (!line.startsWith(m.open, i))
73
+ continue;
74
+ const close = line.indexOf(m.close, i + m.open.length);
75
+ if (close < 0)
76
+ continue; // unterminated → treat as literal
77
+ const inner = line.slice(i + m.open.length, close);
78
+ if (inner.length === 0)
79
+ continue;
80
+ flushPlain(i);
81
+ const isBold = "bold" in m && m.bold;
82
+ const isItalic = "italic" in m && m.italic;
83
+ const isStrike = "strike" in m && m.strike;
84
+ const childBase = {
85
+ ...base,
86
+ bold: base.bold || isBold || undefined,
87
+ italic: base.italic || isItalic || undefined,
88
+ strike: base.strike || isStrike || undefined,
89
+ fg: base.fg ?? (isBold ? t.mdBold : isItalic ? t.mdItalic : isStrike ? t.textMuted : undefined),
90
+ };
91
+ for (const seg of parseInline(inner, t, childBase))
92
+ out.push(seg);
93
+ i = close + m.close.length;
94
+ plainStart = i;
95
+ matched = true;
96
+ break;
97
+ }
98
+ if (matched)
99
+ continue;
100
+ i++;
101
+ }
102
+ flushPlain(line.length);
103
+ return out;
104
+ }
105
+ const FENCE = /^\s*(`{3,}|~{3,})\s*([\w+-]*)\s*$/;
106
+ const HEADING = /^(#{1,6})\s+(.*)$/;
107
+ const HR = /^\s*([-*_])\1{2,}\s*$/;
108
+ const BULLET = /^(\s*)[-*+]\s+(.*)$/;
109
+ const ORDERED = /^(\s*)(\d+)[.)]\s+(.*)$/;
110
+ const QUOTE = /^\s*>\s?(.*)$/;
111
+ function parseBlocks(content, t) {
112
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
113
+ const blocks = [];
114
+ let i = 0;
115
+ while (i < lines.length) {
116
+ const line = lines[i];
117
+ const fence = FENCE.exec(line);
118
+ if (fence) {
119
+ const marker = fence[1];
120
+ const lang = fence[2] ?? "";
121
+ const body = [];
122
+ i++;
123
+ while (i < lines.length && !new RegExp(`^\\s*${marker[0]}{${marker.length},}\\s*$`).test(lines[i])) {
124
+ body.push(lines[i]);
125
+ i++;
126
+ }
127
+ i++; // consume closing fence (or run off the end — streaming-safe)
128
+ blocks.push({ kind: "code", lang, lines: body });
129
+ continue;
130
+ }
131
+ if (line.trim() === "") {
132
+ blocks.push({ kind: "blank" });
133
+ i++;
134
+ continue;
135
+ }
136
+ if (HR.test(line)) {
137
+ blocks.push({ kind: "hr" });
138
+ i++;
139
+ continue;
140
+ }
141
+ const h = HEADING.exec(line);
142
+ if (h) {
143
+ blocks.push({ kind: "heading", level: h[1].length, segs: parseInline(h[2], t, { fg: t.mdHeading, bold: true }) });
144
+ i++;
145
+ continue;
146
+ }
147
+ const q = QUOTE.exec(line);
148
+ if (q) {
149
+ blocks.push({ kind: "quote", segs: parseInline(q[1], t, { fg: t.mdItalic, italic: true }) });
150
+ i++;
151
+ continue;
152
+ }
153
+ const b = BULLET.exec(line);
154
+ if (b) {
155
+ blocks.push({ kind: "bullet", indent: b[1].length, segs: parseInline(b[2], t) });
156
+ i++;
157
+ continue;
158
+ }
159
+ const o = ORDERED.exec(line);
160
+ if (o) {
161
+ blocks.push({ kind: "ordered", indent: o[1].length, marker: `${o[2]}.`, segs: parseInline(o[3], t) });
162
+ i++;
163
+ continue;
164
+ }
165
+ blocks.push({ kind: "para", segs: parseInline(line, t) });
166
+ i++;
167
+ }
168
+ return blocks;
169
+ }
170
+ function Spans({ segs }) {
171
+ // opentui sets bold/italic/underline via the <b>/<i>/<u> text-modifier
172
+ // elements, NOT via span style flags. Compose them around a colored <span>.
173
+ return (_jsx(_Fragment, { children: segs.map((s, i) => {
174
+ // biome-ignore lint/suspicious/noArrayIndexKey: segments are positional within a line
175
+ let node = (_jsx("span", { style: s.fg ? { fg: s.fg } : undefined, children: s.text }, `s${i}`));
176
+ if (s.underline)
177
+ node = _jsx("u", { children: node }, `u${i}`);
178
+ if (s.italic)
179
+ node = _jsx("i", { children: node }, `i${i}`);
180
+ if (s.bold)
181
+ node = _jsx("b", { children: node }, `b${i}`);
182
+ return node;
183
+ }) }));
184
+ }
185
+ /**
186
+ * Render markdown `content` into themed opentui nodes with all syntax markers
187
+ * concealed. Returns a column box; safe to embed inside any flex container.
188
+ */
189
+ export function renderMarkdown(content, t) {
190
+ const blocks = parseBlocks(content, t);
191
+ return (_jsx("box", { flexDirection: "column", flexShrink: 0, children: blocks.map((blk, idx) => {
192
+ // biome-ignore lint/suspicious/noArrayIndexKey: blocks are positional and re-rendered wholesale
193
+ const key = `b${idx}`;
194
+ switch (blk.kind) {
195
+ case "blank":
196
+ return _jsx("box", { height: 1 }, key);
197
+ case "hr":
198
+ return (_jsx("text", { fg: t.mdHr, children: "─".repeat(40) }, key));
199
+ case "heading":
200
+ return (_jsx("text", { marginTop: idx === 0 ? 0 : 1, children: _jsx(Spans, { segs: blk.segs }) }, key));
201
+ case "quote":
202
+ return (_jsxs("text", { children: [_jsx("span", { style: { fg: t.mdHr }, children: "▏ " }), _jsx(Spans, { segs: blk.segs })] }, key));
203
+ case "bullet":
204
+ return (_jsxs("text", { children: [_jsx("span", { style: { fg: t.text }, children: " ".repeat(blk.indent) }), _jsx("span", { style: { fg: t.mdListBullet }, children: "• " }), _jsx(Spans, { segs: blk.segs })] }, key));
205
+ case "ordered":
206
+ return (_jsxs("text", { children: [_jsx("span", { style: { fg: t.text }, children: " ".repeat(blk.indent) }), _jsx("span", { style: { fg: t.mdListBullet }, children: `${blk.marker} ` }), _jsx(Spans, { segs: blk.segs })] }, key));
207
+ case "code": {
208
+ const lang = detectLang(`x.${blk.lang || "txt"}`);
209
+ return (_jsx("box", { backgroundColor: t.mdCodeBlockBg, paddingLeft: 1, paddingRight: 1, flexDirection: "column", children: blk.lines.map((ln, j) => {
210
+ const toks = lang === "plain" ? [] : tokenize(ln, lang, t);
211
+ return (
212
+ // biome-ignore lint/suspicious/noArrayIndexKey: code lines are positional
213
+ _jsx("text", { children: toks.length === 0 ? (_jsx("span", { style: { fg: t.mdCodeBlockFg }, children: ln || " " })) : (toks.map((tok, k) => (
214
+ // biome-ignore lint/suspicious/noArrayIndexKey: token positions are stable per render
215
+ _jsx("span", { style: { fg: tok.fg }, children: tok.text }, `t${k}`)))) }, `c${j}`));
216
+ }) }, key));
217
+ }
218
+ default:
219
+ return (_jsx("text", { children: _jsx(Spans, { segs: blk.segs }) }, key));
220
+ }
221
+ }) }));
222
+ }
223
+ //# sourceMappingURL=markdown-render.js.map
@@ -1,4 +1,14 @@
1
1
  import type { Theme } from "./theme.js";
2
+ /**
3
+ * Render markdown to themed TUI text.
4
+ *
5
+ * Historically this delegated to opentui's `<markdown>` renderable with
6
+ * `conceal`, but the bundled @opentui/core (0.1.107) ignores `conceal` and
7
+ * leaves `**`/`###`/`` ` `` markers visible (and frequently fails to load the
8
+ * tree-sitter wasm for highlighting). We now render markdown ourselves via
9
+ * `renderMarkdown`, which strips markers and applies theme colors. See
10
+ * markdown-render.tsx for the construct coverage.
11
+ */
2
12
  export declare function Markdown({ content, t }: {
3
13
  content: string;
4
14
  t: Theme;
@@ -1,38 +1,15 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { RGBA, SyntaxStyle } from "@opentui/core";
3
- import { useMemo } from "react";
4
- function buildSyntaxStyle(t) {
5
- return SyntaxStyle.fromStyles({
6
- default: { fg: RGBA.fromHex(t.text) },
7
- "markup.heading": { fg: RGBA.fromHex(t.mdHeading), bold: true },
8
- "markup.heading.1": { fg: RGBA.fromHex(t.mdHeading), bold: true },
9
- "markup.heading.2": { fg: RGBA.fromHex(t.mdHeading), bold: true },
10
- "markup.heading.3": { fg: RGBA.fromHex(t.mdHeading), bold: true },
11
- "markup.bold": { fg: RGBA.fromHex(t.mdBold), bold: true },
12
- "markup.italic": { fg: RGBA.fromHex(t.mdItalic), italic: true },
13
- "markup.raw": { fg: RGBA.fromHex(t.mdCode) },
14
- "markup.link": { fg: RGBA.fromHex(t.mdLink), underline: true },
15
- "markup.link.label": { fg: RGBA.fromHex(t.mdLinkText) },
16
- "markup.list": { fg: RGBA.fromHex(t.mdListBullet) },
17
- "markup.quote": { fg: RGBA.fromHex(t.mdItalic), italic: true },
18
- "markup.separator": { fg: RGBA.fromHex(t.mdHr) },
19
- code: { fg: RGBA.fromHex(t.mdCodeBlockFg), bg: RGBA.fromHex(t.mdCodeBlockBg) },
20
- });
21
- }
22
- const TABLE_OPTIONS = {
23
- widthMode: "full",
24
- columnFitter: "balanced",
25
- wrapMode: "word",
26
- cellPadding: 1,
27
- borders: true,
28
- outerBorder: true,
29
- borderStyle: "rounded",
30
- borderColor: "#333333",
31
- };
1
+ import { renderMarkdown } from "./markdown-render.js";
2
+ /**
3
+ * Render markdown to themed TUI text.
4
+ *
5
+ * Historically this delegated to opentui's `<markdown>` renderable with
6
+ * `conceal`, but the bundled @opentui/core (0.1.107) ignores `conceal` and
7
+ * leaves `**`/`###`/`` ` `` markers visible (and frequently fails to load the
8
+ * tree-sitter wasm for highlighting). We now render markdown ourselves via
9
+ * `renderMarkdown`, which strips markers and applies theme colors. See
10
+ * markdown-render.tsx for the construct coverage.
11
+ */
32
12
  export function Markdown({ content, t }) {
33
- const syntaxStyle = useMemo(() => buildSyntaxStyle(t), [t]);
34
- return (_jsx("markdown", { content: content, syntaxStyle: syntaxStyle, conceal: true,
35
- // @ts-expect-error MarkdownProps omits inherited Renderable.selectable; needed for TUI text selection
36
- selectable: true, tableOptions: TABLE_OPTIONS, flexShrink: 0 }));
13
+ return renderMarkdown(content, t);
37
14
  }
38
15
  //# sourceMappingURL=markdown.js.map
@@ -37,10 +37,10 @@ export const handleCouncilInspectSlash = async (args) => {
37
37
  }
38
38
  // Load all system messages for this session — parameterized query prevents SQL injection (T-17-04)
39
39
  const rows = db
40
- .prepare(`SELECT role, message_json, seq, created_at
41
- FROM messages
42
- WHERE session_id = ?
43
- AND role = 'system'
40
+ .prepare(`SELECT role, message_json, seq, created_at
41
+ FROM messages
42
+ WHERE session_id = ?
43
+ AND role = 'system'
44
44
  ORDER BY seq ASC`)
45
45
  .all(sessionId);
46
46
  if (rows.length === 0) {
@@ -25,10 +25,10 @@ function selectInteractionTimeline(sessionId) {
25
25
  try {
26
26
  const db = getDatabase();
27
27
  return db
28
- .prepare(`SELECT event_type, event_subtype, metadata_json, created_at
29
- FROM interaction_logs
30
- WHERE session_id = ?
31
- AND event_type IN ('ui_interaction', 'routing', 'council', 'ee_injection')
28
+ .prepare(`SELECT event_type, event_subtype, metadata_json, created_at
29
+ FROM interaction_logs
30
+ WHERE session_id = ?
31
+ AND event_type IN ('ui_interaction', 'routing', 'council', 'ee_injection')
32
32
  ORDER BY created_at ASC, id ASC`)
33
33
  .all(sessionId);
34
34
  }
@@ -4,3 +4,11 @@ export declare function truncateLine(s: string, n: number): string;
4
4
  export declare function truncateBlock(text: string, maxLines: number): string;
5
5
  export declare function compactTaskLabel(label: string): string;
6
6
  export declare function sanitizeContent(raw: string): string;
7
+ /**
8
+ * Strip stray model self-annotation macros that leak into the user-facing answer
9
+ * but are NOT instructed anywhere in the prompt. Currently: a trailing
10
+ * `\confidence{NN}` macro emitted intermittently by grok-build. Conservative —
11
+ * only the `\confidence{...}` form is removed, so legitimate LaTeX/code in an
12
+ * answer (e.g. `\frac{a}{b}`) is untouched. Fast-pathed: no work when absent.
13
+ */
14
+ export declare function stripStrayModelMacros(text: string): string;
@@ -29,4 +29,20 @@ export function sanitizeContent(raw) {
29
29
  s = s.replace(/\{"success"\s*:\s*(true|false)\s*,\s*"output"\s*:\s*"[\s\S]*$/m, "");
30
30
  return s.trim();
31
31
  }
32
+ /**
33
+ * Strip stray model self-annotation macros that leak into the user-facing answer
34
+ * but are NOT instructed anywhere in the prompt. Currently: a trailing
35
+ * `\confidence{NN}` macro emitted intermittently by grok-build. Conservative —
36
+ * only the `\confidence{...}` form is removed, so legitimate LaTeX/code in an
37
+ * answer (e.g. `\frac{a}{b}`) is untouched. Fast-pathed: no work when absent.
38
+ */
39
+ export function stripStrayModelMacros(text) {
40
+ if (!text || !text.includes("\\confidence"))
41
+ return text;
42
+ // Trailing form (most common) — also swallow the whitespace/newline before it.
43
+ let out = text.replace(/\s*\\confidence\s*\{[^}]*\}\s*$/i, "");
44
+ // Any remaining mid-text occurrences.
45
+ out = out.replace(/\\confidence\s*\{[^}]*\}/gi, "");
46
+ return out;
47
+ }
32
48
  //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { stripStrayModelMacros } from "./text.js";
3
+ describe("stripStrayModelMacros", () => {
4
+ it("strips a trailing \\confidence{NN} macro and the blank line before it", () => {
5
+ const input = "Root cause: parseInt radix.\n\nFix: use radix 10.\n\n\\confidence{85}";
6
+ expect(stripStrayModelMacros(input)).toBe("Root cause: parseInt radix.\n\nFix: use radix 10.");
7
+ });
8
+ it("strips a mid-text \\confidence macro too", () => {
9
+ expect(stripStrayModelMacros("answer \\confidence{90} continues")).toBe("answer continues");
10
+ });
11
+ it("is a no-op when no macro is present (fast path)", () => {
12
+ const clean = "A normal answer with `\\frac{a}{b}` LaTeX that must survive.";
13
+ expect(stripStrayModelMacros(clean)).toBe(clean);
14
+ });
15
+ it("does not touch other backslash macros (conservative)", () => {
16
+ const latex = "Use \\frac{1}{2} and \\sum_{i}.";
17
+ expect(stripStrayModelMacros(latex)).toBe(latex);
18
+ });
19
+ it("handles empty / falsy input", () => {
20
+ expect(stripStrayModelMacros("")).toBe("");
21
+ });
22
+ });
23
+ //# sourceMappingURL=text.test.js.map
@@ -63,26 +63,59 @@ async function ensureUsageFile(filePath) {
63
63
  }
64
64
  }
65
65
  }
66
+ /**
67
+ * In-process serialization per usage.json path.
68
+ *
69
+ * proper-lockfile guards CROSS-process access, but its FS-mtime staleness
70
+ * check can admit a second concurrent critical section under CPU starvation:
71
+ * a holder that is descheduled long enough fails to refresh the lock mtime,
72
+ * a competitor deems the lock stale and steals it, and two reserve() bodies
73
+ * run at once. Reproduced under 6-process contention — exactly one extra
74
+ * reservation slips past a $1.00 cap (6 × $0.18 = $1.08 > cap).
75
+ *
76
+ * A JS promise-chain mutex makes same-process reserve/commit/release bursts
77
+ * strictly serial. This is the dominant real case (10 parallel tool calls in
78
+ * one CLI process) and closes the in-process overshoot window entirely; the
79
+ * file lock still handles genuine multi-process access.
80
+ */
81
+ const pathMutex = new Map();
82
+ function withPathMutex(filePath, fn) {
83
+ const prev = pathMutex.get(filePath) ?? Promise.resolve();
84
+ // Run regardless of the prior caller's outcome (success or rejection).
85
+ const run = prev.then(fn, fn);
86
+ // Stored tail swallows rejection so one failed caller never rejects queued
87
+ // ones; prune the map entry when this tail is the last in the chain.
88
+ const tail = run.then(() => { }, () => { });
89
+ pathMutex.set(filePath, tail);
90
+ void tail.then(() => {
91
+ if (pathMutex.get(filePath) === tail)
92
+ pathMutex.delete(filePath);
93
+ });
94
+ return run;
95
+ }
66
96
  /**
67
97
  * Execute a function under exclusive file lock on usage.json.
68
- * The lock prevents racing readers/writers across CLI processes.
98
+ * The lock prevents racing readers/writers across CLI processes; the
99
+ * in-process mutex serializes concurrent callers within this process.
69
100
  */
70
101
  async function withLock(filePath, fn) {
71
- await ensureUsageFile(filePath);
72
- const releaseLock = await lockfile.lock(filePath, {
73
- retries: { retries: 10, minTimeout: 10, maxTimeout: 100 },
74
- stale: 5_000,
75
- realpath: false,
102
+ return withPathMutex(filePath, async () => {
103
+ await ensureUsageFile(filePath);
104
+ const releaseLock = await lockfile.lock(filePath, {
105
+ retries: { retries: 10, minTimeout: 10, maxTimeout: 100 },
106
+ stale: 5_000,
107
+ realpath: false,
108
+ });
109
+ try {
110
+ const state = (await atomicReadJSON(filePath)) ?? emptyState();
111
+ const { next, result } = await fn(state);
112
+ await atomicWriteJSON(filePath, next);
113
+ return result;
114
+ }
115
+ finally {
116
+ await releaseLock();
117
+ }
76
118
  });
77
- try {
78
- const state = (await atomicReadJSON(filePath)) ?? emptyState();
79
- const { next, result } = await fn(state);
80
- await atomicWriteJSON(filePath, next);
81
- return result;
82
- }
83
- finally {
84
- await releaseLock();
85
- }
86
119
  }
87
120
  /**
88
121
  * Reserve projected token spend against the monthly cap.