muonroi-cli 1.4.1 → 1.6.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 (194) 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/artifact-cache.d.ts +56 -0
  26. package/dist/src/ee/artifact-cache.js +155 -0
  27. package/dist/src/ee/artifact-cache.test.d.ts +1 -0
  28. package/dist/src/ee/artifact-cache.test.js +69 -0
  29. package/dist/src/ee/auth.d.ts +9 -0
  30. package/dist/src/ee/auth.js +19 -0
  31. package/dist/src/ee/ee-onboarding.d.ts +5 -0
  32. package/dist/src/ee/ee-onboarding.js +76 -0
  33. package/dist/src/ee/search.js +7 -5
  34. package/dist/src/ee/search.test.d.ts +1 -0
  35. package/dist/src/ee/search.test.js +23 -0
  36. package/dist/src/generated/version.d.ts +1 -1
  37. package/dist/src/generated/version.js +1 -1
  38. package/dist/src/headless/output.js +6 -4
  39. package/dist/src/headless/output.test.js +4 -3
  40. package/dist/src/index.js +20 -1
  41. package/dist/src/mcp/__tests__/auto-setup.test.js +74 -0
  42. package/dist/src/mcp/__tests__/client-pool.spec.d.ts +1 -0
  43. package/dist/src/mcp/__tests__/client-pool.spec.js +98 -0
  44. package/dist/src/mcp/__tests__/parallel-build.spec.d.ts +1 -0
  45. package/dist/src/mcp/__tests__/parallel-build.spec.js +67 -0
  46. package/dist/src/mcp/__tests__/smart-filter.test.js +56 -0
  47. package/dist/src/mcp/auto-setup.js +56 -2
  48. package/dist/src/mcp/client-pool.d.ts +46 -0
  49. package/dist/src/mcp/client-pool.js +212 -0
  50. package/dist/src/mcp/oauth-callback.js +2 -2
  51. package/dist/src/mcp/parse-headers.test.js +14 -14
  52. package/dist/src/mcp/runtime.d.ts +28 -0
  53. package/dist/src/mcp/runtime.js +117 -51
  54. package/dist/src/mcp/self-verify-runner.d.ts +14 -0
  55. package/dist/src/mcp/self-verify-runner.js +38 -0
  56. package/dist/src/mcp/setup-guide-text.d.ts +9 -0
  57. package/dist/src/mcp/setup-guide-text.js +84 -0
  58. package/dist/src/mcp/smart-filter.js +49 -0
  59. package/dist/src/mcp/smoke.test.js +43 -43
  60. package/dist/src/mcp/tools-server.d.ts +7 -0
  61. package/dist/src/mcp/tools-server.js +19 -22
  62. package/dist/src/models/catalog.json +349 -349
  63. package/dist/src/ops/__tests__/doctor-ee-health.test.js +21 -0
  64. package/dist/src/ops/doctor.d.ts +3 -2
  65. package/dist/src/ops/doctor.js +47 -11
  66. package/dist/src/ops/doctor.test.js +4 -3
  67. package/dist/src/orchestrator/__tests__/mcp-capability-block.test.d.ts +1 -0
  68. package/dist/src/orchestrator/__tests__/mcp-capability-block.test.js +39 -0
  69. package/dist/src/orchestrator/__tests__/project-stack.test.d.ts +1 -0
  70. package/dist/src/orchestrator/__tests__/project-stack.test.js +65 -0
  71. package/dist/src/orchestrator/batch-turn-runner.js +7 -11
  72. package/dist/src/orchestrator/compaction.d.ts +2 -0
  73. package/dist/src/orchestrator/compaction.js +14 -1
  74. package/dist/src/orchestrator/compaction.test.js +25 -1
  75. package/dist/src/orchestrator/message-processor.js +72 -32
  76. package/dist/src/orchestrator/orchestrator.js +26 -0
  77. package/dist/src/orchestrator/prompts.d.ts +51 -0
  78. package/dist/src/orchestrator/prompts.js +257 -134
  79. package/dist/src/orchestrator/scope-ceiling.js +6 -1
  80. package/dist/src/orchestrator/scope-reminder.d.ts +12 -0
  81. package/dist/src/orchestrator/scope-reminder.js +16 -0
  82. package/dist/src/orchestrator/scope-reminder.test.js +22 -1
  83. package/dist/src/orchestrator/stream-runner.js +23 -15
  84. package/dist/src/orchestrator/subagent-compactor.d.ts +14 -5
  85. package/dist/src/orchestrator/subagent-compactor.js +30 -8
  86. package/dist/src/orchestrator/subagent-compactor.spec.js +18 -0
  87. package/dist/src/orchestrator/text-tool-call-detector.test.js +13 -13
  88. package/dist/src/pil/__tests__/clarity-gate.test.js +24 -215
  89. package/dist/src/pil/__tests__/config.test.js +1 -17
  90. package/dist/src/pil/__tests__/discovery.test.js +144 -11
  91. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +7 -2
  92. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  93. package/dist/src/pil/__tests__/layer16-clarity.test.js +32 -116
  94. package/dist/src/pil/__tests__/layer4-gsd.test.js +37 -0
  95. package/dist/src/pil/__tests__/layer6-output.test.js +158 -18
  96. package/dist/src/pil/__tests__/llm-classify.test.js +49 -2
  97. package/dist/src/pil/__tests__/surface-compaction-artifacts.test.d.ts +1 -0
  98. package/dist/src/pil/__tests__/surface-compaction-artifacts.test.js +112 -0
  99. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  100. package/dist/src/pil/agent-operating-contract.js +2 -0
  101. package/dist/src/pil/agent-operating-contract.test.js +7 -2
  102. package/dist/src/pil/cheap-model-playbook.js +35 -35
  103. package/dist/src/pil/cheap-model-workbooks.js +16 -13
  104. package/dist/src/pil/clarity-gate.d.ts +21 -19
  105. package/dist/src/pil/clarity-gate.js +26 -153
  106. package/dist/src/pil/config.d.ts +9 -1
  107. package/dist/src/pil/config.js +15 -4
  108. package/dist/src/pil/discovery.js +211 -136
  109. package/dist/src/pil/layer1-intent.d.ts +12 -0
  110. package/dist/src/pil/layer1-intent.js +283 -38
  111. package/dist/src/pil/layer1-intent.test.js +210 -4
  112. package/dist/src/pil/layer16-clarity.d.ts +25 -11
  113. package/dist/src/pil/layer16-clarity.js +19 -306
  114. package/dist/src/pil/layer3-ee-injection.d.ts +19 -0
  115. package/dist/src/pil/layer3-ee-injection.js +96 -4
  116. package/dist/src/pil/layer4-gsd.js +18 -6
  117. package/dist/src/pil/layer6-output.d.ts +2 -0
  118. package/dist/src/pil/layer6-output.js +151 -25
  119. package/dist/src/pil/llm-classify.d.ts +26 -0
  120. package/dist/src/pil/llm-classify.js +34 -5
  121. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  122. package/dist/src/pil/native-capabilities-workbook.js +82 -76
  123. package/dist/src/pil/pipeline.js +15 -9
  124. package/dist/src/pil/schema.d.ts +8 -0
  125. package/dist/src/pil/schema.js +12 -1
  126. package/dist/src/pil/task-tier-map.js +4 -0
  127. package/dist/src/pil/types.d.ts +11 -1
  128. package/dist/src/product-loop/done-gate.js +3 -3
  129. package/dist/src/product-loop/loop-driver.js +18 -18
  130. package/dist/src/product-loop/progress-snapshot.js +4 -4
  131. package/dist/src/providers/auth/gemini-oauth.js +6 -15
  132. package/dist/src/providers/auth/grok-oauth.js +6 -15
  133. package/dist/src/providers/auth/openai-oauth.js +6 -15
  134. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  135. package/dist/src/reporter/index.js +1 -1
  136. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  137. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  138. package/dist/src/scaffold/continuation-prompt.js +60 -60
  139. package/dist/src/scaffold/init-new.js +453 -453
  140. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  141. package/dist/src/self-qa/agentic-loop.js +24 -19
  142. package/dist/src/self-qa/spec-emitter.js +26 -23
  143. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  144. package/dist/src/storage/interaction-log.js +5 -5
  145. package/dist/src/storage/migrations.js +122 -122
  146. package/dist/src/storage/sessions.js +42 -42
  147. package/dist/src/storage/transcript.js +91 -84
  148. package/dist/src/storage/usage.js +14 -14
  149. package/dist/src/storage/workspaces.js +12 -12
  150. package/dist/src/tools/__tests__/native-tools.test.d.ts +1 -0
  151. package/dist/src/tools/__tests__/native-tools.test.js +53 -0
  152. package/dist/src/tools/git-safety.d.ts +61 -0
  153. package/dist/src/tools/git-safety.js +141 -0
  154. package/dist/src/tools/git-safety.test.d.ts +1 -0
  155. package/dist/src/tools/git-safety.test.js +111 -0
  156. package/dist/src/tools/native-tools.d.ts +31 -0
  157. package/dist/src/tools/native-tools.js +273 -0
  158. package/dist/src/tools/registry-ee-query.test.js +18 -1
  159. package/dist/src/tools/registry-git-safety.test.d.ts +7 -0
  160. package/dist/src/tools/registry-git-safety.test.js +92 -0
  161. package/dist/src/tools/registry.js +52 -6
  162. package/dist/src/ui/__tests__/markdown-render.test.d.ts +1 -0
  163. package/dist/src/ui/__tests__/markdown-render.test.js +48 -0
  164. package/dist/src/ui/app.js +0 -0
  165. package/dist/src/ui/components/message-view.js +4 -1
  166. package/dist/src/ui/components/structured-response-view.js +7 -3
  167. package/dist/src/ui/components/tool-group.js +7 -1
  168. package/dist/src/ui/markdown-render.d.ts +41 -0
  169. package/dist/src/ui/markdown-render.js +223 -0
  170. package/dist/src/ui/markdown.d.ts +10 -0
  171. package/dist/src/ui/markdown.js +12 -35
  172. package/dist/src/ui/slash/council-inspect.js +4 -4
  173. package/dist/src/ui/slash/export.js +4 -4
  174. package/dist/src/ui/utils/text.d.ts +8 -0
  175. package/dist/src/ui/utils/text.js +16 -0
  176. package/dist/src/ui/utils/text.test.d.ts +1 -0
  177. package/dist/src/ui/utils/text.test.js +23 -0
  178. package/dist/src/usage/ledger.js +48 -15
  179. package/dist/src/utils/__tests__/footprint-gitignore.test.d.ts +1 -0
  180. package/dist/src/utils/__tests__/footprint-gitignore.test.js +50 -0
  181. package/dist/src/utils/clipboard-image.js +23 -23
  182. package/dist/src/utils/open-url.d.ts +56 -0
  183. package/dist/src/utils/open-url.js +58 -0
  184. package/dist/src/utils/open-url.test.d.ts +1 -0
  185. package/dist/src/utils/open-url.test.js +86 -0
  186. package/dist/src/utils/settings.d.ts +12 -0
  187. package/dist/src/utils/settings.js +48 -0
  188. package/dist/src/utils/side-question.js +2 -2
  189. package/dist/src/utils/skills.js +3 -3
  190. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  191. package/dist/src/verify/environment.js +2 -1
  192. package/package.json +1 -1
  193. package/dist/src/pil/layer16-clarity.test.js +0 -31
  194. /package/dist/src/{pil/layer16-clarity.test.d.ts → council/__tests__/clarification-prompt.test.d.ts} +0 -0
@@ -42,11 +42,11 @@ function isInternalCouncilMarker(content) {
42
42
  }
43
43
  function loadMessageRows(sessionId) {
44
44
  const rows = getDatabase()
45
- .prepare(`
46
- SELECT session_id, seq, role, message_json, created_at
47
- FROM messages
48
- WHERE session_id = ?
49
- ORDER BY seq ASC
45
+ .prepare(`
46
+ SELECT session_id, seq, role, message_json, created_at
47
+ FROM messages
48
+ WHERE session_id = ?
49
+ ORDER BY seq ASC
50
50
  `)
51
51
  .all(sessionId);
52
52
  for (const row of rows) {
@@ -66,12 +66,12 @@ function toPersistedCompaction(row) {
66
66
  }
67
67
  export function loadLatestCompaction(sessionId) {
68
68
  const row = getDatabase()
69
- .prepare(`
70
- SELECT session_id, first_kept_seq, summary, tokens_before, created_at
71
- FROM compactions
72
- WHERE session_id = ?
73
- ORDER BY id DESC
74
- LIMIT 1
69
+ .prepare(`
70
+ SELECT session_id, first_kept_seq, summary, tokens_before, created_at
71
+ FROM compactions
72
+ WHERE session_id = ?
73
+ ORDER BY id DESC
74
+ LIMIT 1
75
75
  `)
76
76
  .get(sessionId);
77
77
  return toPersistedCompaction(row);
@@ -112,37 +112,37 @@ export function appendMessages(sessionId, messages) {
112
112
  // with `status='completed'` and the full message_json. Pre-A5 callers
113
113
  // hit this path identically: there is no pending row so the IGNORE
114
114
  // branch is unused and the INSERT wins.
115
- const insertMessage = db.prepare(`
116
- INSERT INTO messages (session_id, seq, role, message_json, created_at, status)
117
- VALUES (?, ?, ?, ?, ?, 'completed')
118
- ON CONFLICT(session_id, seq) DO UPDATE SET
119
- role = excluded.role,
120
- message_json = excluded.message_json,
121
- status = 'completed'
115
+ const insertMessage = db.prepare(`
116
+ INSERT INTO messages (session_id, seq, role, message_json, created_at, status)
117
+ VALUES (?, ?, ?, ?, ?, 'completed')
118
+ ON CONFLICT(session_id, seq) DO UPDATE SET
119
+ role = excluded.role,
120
+ message_json = excluded.message_json,
121
+ status = 'completed'
122
122
  `);
123
- const insertToolCall = db.prepare(`
124
- INSERT OR IGNORE INTO tool_calls (
125
- session_id, message_seq, tool_call_id, tool_name, args_json, status, started_at, completed_at
126
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
123
+ const insertToolCall = db.prepare(`
124
+ INSERT OR IGNORE INTO tool_calls (
125
+ session_id, message_seq, tool_call_id, tool_name, args_json, status, started_at, completed_at
126
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
127
127
  `);
128
- const updateToolCall = db.prepare(`
129
- UPDATE tool_calls
130
- SET tool_name = ?, args_json = ?, status = ?, completed_at = ?
131
- WHERE session_id = ? AND tool_call_id = ?
128
+ const updateToolCall = db.prepare(`
129
+ UPDATE tool_calls
130
+ SET tool_name = ?, args_json = ?, status = ?, completed_at = ?
131
+ WHERE session_id = ? AND tool_call_id = ?
132
132
  `);
133
- const selectToolCall = db.prepare(`
134
- SELECT id, tool_call_id, tool_name, args_json
135
- FROM tool_calls
136
- WHERE session_id = ? AND tool_call_id = ?
133
+ const selectToolCall = db.prepare(`
134
+ SELECT id, tool_call_id, tool_name, args_json
135
+ FROM tool_calls
136
+ WHERE session_id = ? AND tool_call_id = ?
137
137
  `);
138
- const insertToolResult = db.prepare(`
139
- INSERT INTO tool_results (tool_call_row_id, output_kind, output_json, success, created_at)
140
- VALUES (?, ?, ?, ?, ?)
138
+ const insertToolResult = db.prepare(`
139
+ INSERT INTO tool_results (tool_call_row_id, output_kind, output_json, success, created_at)
140
+ VALUES (?, ?, ?, ?, ?)
141
141
  `);
142
- const updateSession = db.prepare(`
143
- UPDATE sessions
144
- SET updated_at = ?
145
- WHERE id = ?
142
+ const updateSession = db.prepare(`
143
+ UPDATE sessions
144
+ SET updated_at = ?
145
+ WHERE id = ?
146
146
  `);
147
147
  messages.forEach((message, index) => {
148
148
  const seq = nextSeq + index;
@@ -204,10 +204,10 @@ export function persistToolCallWriteAhead(sessionId, messageSeq, toolCallId, too
204
204
  const now = new Date().toISOString();
205
205
  try {
206
206
  getDatabase()
207
- .prepare(`
208
- INSERT OR IGNORE INTO tool_calls (
209
- session_id, message_seq, tool_call_id, tool_name, args_json, status, started_at, completed_at
210
- ) VALUES (?, ?, ?, ?, ?, 'pending', ?, NULL)
207
+ .prepare(`
208
+ INSERT OR IGNORE INTO tool_calls (
209
+ session_id, message_seq, tool_call_id, tool_name, args_json, status, started_at, completed_at
210
+ ) VALUES (?, ?, ?, ?, ?, 'pending', ?, NULL)
211
211
  `)
212
212
  .run(sessionId, messageSeq, toolCallId, toolName, argsJson, now);
213
213
  }
@@ -253,10 +253,10 @@ export function persistMessageWriteAhead(sessionId, seq, role, messageJson) {
253
253
  const now = new Date().toISOString();
254
254
  try {
255
255
  getDatabase()
256
- .prepare(`
257
- INSERT OR IGNORE INTO messages (
258
- session_id, seq, role, message_json, created_at, status
259
- ) VALUES (?, ?, ?, ?, ?, 'pending')
256
+ .prepare(`
257
+ INSERT OR IGNORE INTO messages (
258
+ session_id, seq, role, message_json, created_at, status
259
+ ) VALUES (?, ?, ?, ?, ?, 'pending')
260
260
  `)
261
261
  .run(sessionId, seq, role, messageJson, now);
262
262
  }
@@ -274,10 +274,10 @@ export function persistMessageWriteAhead(sessionId, seq, role, messageJson) {
274
274
  export function markMessageCompleted(sessionId, seq) {
275
275
  try {
276
276
  getDatabase()
277
- .prepare(`
278
- UPDATE messages
279
- SET status = 'completed'
280
- WHERE session_id = ? AND seq = ? AND status = 'pending'
277
+ .prepare(`
278
+ UPDATE messages
279
+ SET status = 'completed'
280
+ WHERE session_id = ? AND seq = ? AND status = 'pending'
281
281
  `)
282
282
  .run(sessionId, seq);
283
283
  }
@@ -296,10 +296,10 @@ export function markMessageCompleted(sessionId, seq) {
296
296
  export function markMessageErrored(sessionId, seq) {
297
297
  try {
298
298
  getDatabase()
299
- .prepare(`
300
- UPDATE messages
301
- SET status = 'errored'
302
- WHERE session_id = ? AND seq = ? AND status = 'pending'
299
+ .prepare(`
300
+ UPDATE messages
301
+ SET status = 'errored'
302
+ WHERE session_id = ? AND seq = ? AND status = 'pending'
303
303
  `)
304
304
  .run(sessionId, seq);
305
305
  }
@@ -331,17 +331,17 @@ export function sweepStalePendingRows(staleAfterMs = 5 * 60 * 1000) {
331
331
  try {
332
332
  const db = getDatabase();
333
333
  const toolCalls = db
334
- .prepare(`
335
- UPDATE tool_calls
336
- SET status = 'aborted', completed_at = ?
337
- WHERE status = 'pending' AND started_at < ?
334
+ .prepare(`
335
+ UPDATE tool_calls
336
+ SET status = 'aborted', completed_at = ?
337
+ WHERE status = 'pending' AND started_at < ?
338
338
  `)
339
339
  .run(now, cutoff);
340
340
  const messages = db
341
- .prepare(`
342
- UPDATE messages
343
- SET status = 'aborted'
344
- WHERE status = 'pending' AND created_at < ?
341
+ .prepare(`
342
+ UPDATE messages
343
+ SET status = 'aborted'
344
+ WHERE status = 'pending' AND created_at < ?
345
345
  `)
346
346
  .run(cutoff);
347
347
  return { toolCalls: toolCalls.changes, messages: messages.changes };
@@ -355,10 +355,10 @@ export function markToolCallErrored(sessionId, toolCallId, errorMessage) {
355
355
  const now = new Date().toISOString();
356
356
  try {
357
357
  getDatabase()
358
- .prepare(`
359
- UPDATE tool_calls
360
- SET status = 'errored', completed_at = ?, args_json = COALESCE(args_json, ?)
361
- WHERE session_id = ? AND tool_call_id = ?
358
+ .prepare(`
359
+ UPDATE tool_calls
360
+ SET status = 'errored', completed_at = ?, args_json = COALESCE(args_json, ?)
361
+ WHERE session_id = ? AND tool_call_id = ?
362
362
  `)
363
363
  .run(now, JSON.stringify({ error: errorMessage.slice(0, 500) }), sessionId, toolCallId);
364
364
  }
@@ -368,14 +368,14 @@ export function markToolCallErrored(sessionId, toolCallId, errorMessage) {
368
368
  }
369
369
  export function appendCompaction(sessionId, firstKeptSeq, summary, tokensBefore) {
370
370
  withTransaction((db) => {
371
- db.prepare(`
372
- INSERT INTO compactions (session_id, first_kept_seq, summary, tokens_before, created_at)
373
- VALUES (?, ?, ?, ?, ?)
371
+ db.prepare(`
372
+ INSERT INTO compactions (session_id, first_kept_seq, summary, tokens_before, created_at)
373
+ VALUES (?, ?, ?, ?, ?)
374
374
  `).run(sessionId, firstKeptSeq, summary, tokensBefore, new Date().toISOString());
375
- db.prepare(`
376
- UPDATE sessions
377
- SET updated_at = ?
378
- WHERE id = ?
375
+ db.prepare(`
376
+ UPDATE sessions
377
+ SET updated_at = ?
378
+ WHERE id = ?
379
379
  `).run(new Date().toISOString(), sessionId);
380
380
  });
381
381
  }
@@ -393,9 +393,16 @@ export function buildChatEntries(sessionId) {
393
393
  continue;
394
394
  }
395
395
  if (message.role === "system") {
396
- const content = getCompactionSummaryText(message) ?? (typeof message.content === "string" ? message.content.trim() : "");
396
+ const summaryText = getCompactionSummaryText(message);
397
+ const content = summaryText ?? (typeof message.content === "string" ? message.content.trim() : "");
397
398
  if (content && !isInternalCouncilMarker(content)) {
398
- entries.push({ type: "assistant", content, timestamp });
399
+ // A compaction checkpoint is internal context-management, NOT the
400
+ // assistant's answer. Tag it with a source label so the UI renders it
401
+ // as a clearly-marked, collapsible checkpoint instead of dumping the
402
+ // raw "## Goal / ## Context For Suffix" scaffolding as a chat reply.
403
+ entries.push(summaryText !== null
404
+ ? { type: "assistant", content, timestamp, sourceLabel: "⋯ context checkpoint (auto-compacted)" }
405
+ : { type: "assistant", content, timestamp });
399
406
  }
400
407
  continue;
401
408
  }
@@ -460,10 +467,10 @@ export function buildChatEntries(sessionId) {
460
467
  }
461
468
  function getNextSequence(db, sessionId) {
462
469
  const row = db
463
- .prepare(`
464
- SELECT COALESCE(MAX(seq), 0) AS max_seq
465
- FROM messages
466
- WHERE session_id = ?
470
+ .prepare(`
471
+ SELECT COALESCE(MAX(seq), 0) AS max_seq
472
+ FROM messages
473
+ WHERE session_id = ?
467
474
  `)
468
475
  .get(sessionId);
469
476
  return (row?.max_seq ?? 0) + 1;
@@ -513,12 +520,12 @@ function renderAssistantContent(content, callMap) {
513
520
  }
514
521
  function loadStoredToolResults(sessionId) {
515
522
  const rows = getDatabase()
516
- .prepare(`
517
- SELECT tc.tool_call_id, tr.output_json
518
- FROM tool_results tr
519
- JOIN tool_calls tc ON tc.id = tr.tool_call_row_id
520
- WHERE tc.session_id = ?
521
- ORDER BY tr.id ASC
523
+ .prepare(`
524
+ SELECT tc.tool_call_id, tr.output_json
525
+ FROM tool_results tr
526
+ JOIN tool_calls tc ON tc.id = tr.tool_call_row_id
527
+ WHERE tc.session_id = ?
528
+ ORDER BY tr.id ASC
522
529
  `)
523
530
  .all(sessionId);
524
531
  return new Map(rows.map((row) => [row.tool_call_id, JSON.parse(row.output_json)]));
@@ -12,31 +12,31 @@ export function recordUsageEvent(sessionId, source, model, usage, messageSeq, pi
12
12
  const cacheCreationTokens = usage.cacheCreationTokens ?? 0;
13
13
  const costMicros = estimateCostMicros(model, inputTokens, outputTokens, cacheReadTokens);
14
14
  getDatabase()
15
- .prepare(`
16
- INSERT INTO usage_events (
17
- session_id, message_seq, source, model, input_tokens, output_tokens, total_tokens, cost_micros, created_at,
18
- pil_active, enrichment_delta, cache_read_tokens, cache_creation_tokens, provider_options_shape
19
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
15
+ .prepare(`
16
+ INSERT INTO usage_events (
17
+ session_id, message_seq, source, model, input_tokens, output_tokens, total_tokens, cost_micros, created_at,
18
+ pil_active, enrichment_delta, cache_read_tokens, cache_creation_tokens, provider_options_shape
19
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
20
20
  `)
21
21
  .run(sessionId, messageSeq ?? null, source, model, inputTokens, outputTokens, totalTokens, costMicros, new Date().toISOString(), pilActive ? 1 : 0, enrichmentDelta, cacheReadTokens, cacheCreationTokens, providerOptionsShape);
22
22
  }
23
23
  export function getSessionTotalTokens(sessionId) {
24
24
  const row = getDatabase()
25
- .prepare(`
26
- SELECT COALESCE(SUM(total_tokens), 0) AS total_tokens
27
- FROM usage_events
28
- WHERE session_id = ?
25
+ .prepare(`
26
+ SELECT COALESCE(SUM(total_tokens), 0) AS total_tokens
27
+ FROM usage_events
28
+ WHERE session_id = ?
29
29
  `)
30
30
  .get(sessionId);
31
31
  return row?.total_tokens ?? 0;
32
32
  }
33
33
  export function listSessionUsage(sessionId) {
34
34
  const rows = getDatabase()
35
- .prepare(`
36
- SELECT id, session_id, message_seq, source, model, input_tokens, output_tokens, total_tokens, cost_micros, created_at, cache_read_tokens, cache_creation_tokens
37
- FROM usage_events
38
- WHERE session_id = ?
39
- ORDER BY id ASC
35
+ .prepare(`
36
+ SELECT id, session_id, message_seq, source, model, input_tokens, output_tokens, total_tokens, cost_micros, created_at, cache_read_tokens, cache_creation_tokens
37
+ FROM usage_events
38
+ WHERE session_id = ?
39
+ ORDER BY id ASC
40
40
  `)
41
41
  .all(sessionId);
42
42
  return rows.map((row) => ({
@@ -8,14 +8,14 @@ export function ensureWorkspace(cwd) {
8
8
  const now = new Date().toISOString();
9
9
  const id = createHash("sha1").update(resolved.scopeKey).digest("hex").slice(0, 16);
10
10
  const db = getDatabase();
11
- db.prepare(`
12
- INSERT INTO workspaces (id, scope_key, canonical_path, git_root, display_name, last_seen_at)
13
- VALUES (@id, @scope_key, @canonical_path, @git_root, @display_name, @last_seen_at)
14
- ON CONFLICT(scope_key) DO UPDATE SET
15
- canonical_path = excluded.canonical_path,
16
- git_root = excluded.git_root,
17
- display_name = excluded.display_name,
18
- last_seen_at = excluded.last_seen_at
11
+ db.prepare(`
12
+ INSERT INTO workspaces (id, scope_key, canonical_path, git_root, display_name, last_seen_at)
13
+ VALUES (@id, @scope_key, @canonical_path, @git_root, @display_name, @last_seen_at)
14
+ ON CONFLICT(scope_key) DO UPDATE SET
15
+ canonical_path = excluded.canonical_path,
16
+ git_root = excluded.git_root,
17
+ display_name = excluded.display_name,
18
+ last_seen_at = excluded.last_seen_at
19
19
  `).run({
20
20
  id,
21
21
  scope_key: resolved.scopeKey,
@@ -25,10 +25,10 @@ export function ensureWorkspace(cwd) {
25
25
  last_seen_at: now,
26
26
  });
27
27
  const row = db
28
- .prepare(`
29
- SELECT id, scope_key, canonical_path, git_root, display_name, last_seen_at
30
- FROM workspaces
31
- WHERE scope_key = ?
28
+ .prepare(`
29
+ SELECT id, scope_key, canonical_path, git_root, display_name, last_seen_at
30
+ FROM workspaces
31
+ WHERE scope_key = ?
32
32
  `)
33
33
  .get(resolved.scopeKey);
34
34
  if (!row) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { NATIVE_MUONROI_TOOL_NAMES, registerNativeMuonroiTools } from "../native-tools.js";
3
+ async function exec(tools, name, args) {
4
+ const tool = tools[name];
5
+ return (await tool.execute(args, {}));
6
+ }
7
+ describe("registerNativeMuonroiTools", () => {
8
+ it("registers every muonroi-tools capability as a native builtin", () => {
9
+ const tools = {};
10
+ registerNativeMuonroiTools(tools);
11
+ for (const name of NATIVE_MUONROI_TOOL_NAMES) {
12
+ expect(tools[name], `missing native tool ${name}`).toBeDefined();
13
+ expect(typeof tools[name].execute).toBe("function");
14
+ }
15
+ // ee_query stays the registry's own native tool — not duplicated here.
16
+ expect(tools.ee_query).toBeUndefined();
17
+ });
18
+ it("setup_guide returns the shared guide text (no MCP round-trip)", async () => {
19
+ const tools = {};
20
+ registerNativeMuonroiTools(tools);
21
+ const out = await exec(tools, "setup_guide", {});
22
+ expect(out).toContain("muonroi-cli Setup Guide");
23
+ expect(out).toContain("muonroi-cli doctor");
24
+ });
25
+ it("ee_feedback rejects a noise verdict with no reason (no network call)", async () => {
26
+ const tools = {};
27
+ registerNativeMuonroiTools(tools);
28
+ const out = await exec(tools, "ee_feedback", { id: "abc", collection: "experience-behavioral", verdict: "noise" });
29
+ expect(out).toMatch(/reason_required/);
30
+ });
31
+ it("ee_feedback validates required args", async () => {
32
+ const tools = {};
33
+ registerNativeMuonroiTools(tools);
34
+ expect(await exec(tools, "ee_feedback", { verdict: "followed" })).toMatch(/invalid_args/);
35
+ });
36
+ it("selfverify_status returns not_found for an unknown runId (shared JobManager)", async () => {
37
+ const tools = {};
38
+ registerNativeMuonroiTools(tools);
39
+ const out = await exec(tools, "selfverify_status", { runId: "does-not-exist" });
40
+ expect(out).toMatch(/not_found/);
41
+ });
42
+ it("selfverify_start rejects agentic mode without goal+llm", async () => {
43
+ const tools = {};
44
+ registerNativeMuonroiTools(tools);
45
+ expect(await exec(tools, "selfverify_start", { mode: "agentic" })).toMatch(/invalid_args/);
46
+ });
47
+ it("usage_forensics rejects an empty prefix without touching the DB", async () => {
48
+ const tools = {};
49
+ registerNativeMuonroiTools(tools);
50
+ expect(await exec(tools, "usage_forensics", { prefix: " " })).toMatch(/invalid_args/);
51
+ });
52
+ });
53
+ //# sourceMappingURL=native-tools.test.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * src/tools/git-safety.ts
3
+ *
4
+ * Pre-execution git safety for the bash tool. Distilled from a real session
5
+ * audit (18285908637a) where a cheap model:
6
+ * 1. pushed while 24 tests were failing (it batched `git add -A && commit &&
7
+ * push` in the SAME tool batch as the test run, so push was never gated
8
+ * on the result), and
9
+ * 2. swept the CLI's own `.muonroi-cli/settings.json` (which can hold
10
+ * provider API keys) into a public repo via `git add -A`.
11
+ *
12
+ * Two guards, both cheap and side-effect-free:
13
+ * - PUSH GATE: a session-scoped record of failed verification commands
14
+ * (test/build/lint/typecheck). A `git push` is BLOCKED (not executed)
15
+ * while any verification command has failed this session and not been
16
+ * re-run green. Mirrors the repo's mandatory Pre-Push Test Gate.
17
+ * - STAGING WARNING: a non-blocking warning when a broad `git add -A` /
18
+ * `git add .` / `git commit -a` is run while sensitive paths (`.env`,
19
+ * `.muonroi-cli/`, private keys, credentials) exist in the repo root.
20
+ *
21
+ * The push gate is deterministic for the sequential case (a failed check then
22
+ * a later push). The concurrent case (push batched in the same parallel tool
23
+ * call as the check) is covered by the system-prompt git-safety clause.
24
+ *
25
+ * Override the push gate with `MUONROI_ALLOW_PUSH_ON_RED=1`.
26
+ */
27
+ interface GitSafetyEntry {
28
+ /** canonical verification command -> epoch ms it last failed. */
29
+ failedVerifies: Map<string, number>;
30
+ }
31
+ declare global {
32
+ var __muonroiGitSafetyState: Map<string, GitSafetyEntry> | undefined;
33
+ }
34
+ /** Test helper — clear all session git-safety state. */
35
+ export declare function __resetGitSafetyState(): void;
36
+ export interface GitCommandShape {
37
+ isPush: boolean;
38
+ /** `git add -A` / `git add .` / `git add --all` / `git commit -a[m]`. */
39
+ isBroadStage: boolean;
40
+ }
41
+ export declare function analyzeGitCommand(command: string): GitCommandShape;
42
+ /**
43
+ * Record the outcome of a bash command for the push gate. Only verification
44
+ * commands (test/build/lint/typecheck) are tracked. A pass clears that exact
45
+ * command's failed flag; a failure sets it. Non-verification commands are
46
+ * ignored.
47
+ */
48
+ export declare function recordCommandOutcome(sessionKey: string, canonical: string, success: boolean): void;
49
+ export interface PushGateResult {
50
+ blocked: boolean;
51
+ /** Canonical commands that failed and gate the push. */
52
+ failed: string[];
53
+ }
54
+ export declare function checkPushGate(sessionKey: string): PushGateResult;
55
+ /** Message returned in place of running a blocked `git push`. */
56
+ export declare function pushBlockedMessage(failed: string[]): string;
57
+ /** Sensitive paths present in `cwd` that a broad `git add` would likely sweep in. */
58
+ export declare function detectSensitiveStaging(cwd: string): string[];
59
+ /** Non-blocking warning appended to a broad-stage command's output, or "". */
60
+ export declare function stagingWarning(cwd: string): string;
61
+ export {};
@@ -0,0 +1,141 @@
1
+ /**
2
+ * src/tools/git-safety.ts
3
+ *
4
+ * Pre-execution git safety for the bash tool. Distilled from a real session
5
+ * audit (18285908637a) where a cheap model:
6
+ * 1. pushed while 24 tests were failing (it batched `git add -A && commit &&
7
+ * push` in the SAME tool batch as the test run, so push was never gated
8
+ * on the result), and
9
+ * 2. swept the CLI's own `.muonroi-cli/settings.json` (which can hold
10
+ * provider API keys) into a public repo via `git add -A`.
11
+ *
12
+ * Two guards, both cheap and side-effect-free:
13
+ * - PUSH GATE: a session-scoped record of failed verification commands
14
+ * (test/build/lint/typecheck). A `git push` is BLOCKED (not executed)
15
+ * while any verification command has failed this session and not been
16
+ * re-run green. Mirrors the repo's mandatory Pre-Push Test Gate.
17
+ * - STAGING WARNING: a non-blocking warning when a broad `git add -A` /
18
+ * `git add .` / `git commit -a` is run while sensitive paths (`.env`,
19
+ * `.muonroi-cli/`, private keys, credentials) exist in the repo root.
20
+ *
21
+ * The push gate is deterministic for the sequential case (a failed check then
22
+ * a later push). The concurrent case (push batched in the same parallel tool
23
+ * call as the check) is covered by the system-prompt git-safety clause.
24
+ *
25
+ * Override the push gate with `MUONROI_ALLOW_PUSH_ON_RED=1`.
26
+ */
27
+ import * as fs from "node:fs";
28
+ import * as path from "node:path";
29
+ import { isVerificationCommand } from "../orchestrator/tool-args-hash.js";
30
+ function getState() {
31
+ if (!globalThis.__muonroiGitSafetyState) {
32
+ globalThis.__muonroiGitSafetyState = new Map();
33
+ }
34
+ return globalThis.__muonroiGitSafetyState;
35
+ }
36
+ /** Test helper — clear all session git-safety state. */
37
+ export function __resetGitSafetyState() {
38
+ globalThis.__muonroiGitSafetyState = new Map();
39
+ }
40
+ /**
41
+ * Blank out single/double-quoted substrings so a commit MESSAGE that merely
42
+ * mentions "git push" or "-a" never trips the classifiers. Cheap and good
43
+ * enough — we only need to avoid matching inside obvious quoted args.
44
+ */
45
+ function stripQuoted(command) {
46
+ return command.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''");
47
+ }
48
+ // Each classifier scopes to ONE shell clause: `[^|&;\n]*` stops at pipes,
49
+ // `&`/`;`, AND newlines (all bash clause separators), so a separate command on
50
+ // another line (`git status\necho push`) cannot bleed into a false match. The
51
+ // `[ \t]` before the subcommand (not `\s`) keeps the match on one line and
52
+ // avoids `--grep=push` / `=push`. Quote-stripping upstream avoids commit-message
53
+ // hits. Allowing `[^|&;\n]*` between `git` and the subcommand covers global
54
+ // options like `git -c key=val push`.
55
+ const PUSH_RE = /\bgit\b[^|&;\n]*[ \t]push\b/;
56
+ const BROAD_ADD_RE = /\bgit\b[^|&;\n]*[ \t]add[ \t]+(?:-A\b|--all\b|\.(?=[ \t]|$))/;
57
+ // `-a` / `-am` / `-ma` etc. — a short-flag cluster CONTAINING `a`, terminated by
58
+ // whitespace or end (so `-a--otherflag` and `--amend` do NOT match).
59
+ const COMMIT_ALL_RE = /\bgit\b[^|&;\n]*[ \t]commit\b[^|&;\n]*[ \t]-[a-z]*a[a-z]*(?=[ \t]|$)/;
60
+ export function analyzeGitCommand(command) {
61
+ const c = stripQuoted(command);
62
+ return {
63
+ isPush: PUSH_RE.test(c),
64
+ isBroadStage: BROAD_ADD_RE.test(c) || COMMIT_ALL_RE.test(c),
65
+ };
66
+ }
67
+ /**
68
+ * Record the outcome of a bash command for the push gate. Only verification
69
+ * commands (test/build/lint/typecheck) are tracked. A pass clears that exact
70
+ * command's failed flag; a failure sets it. Non-verification commands are
71
+ * ignored.
72
+ */
73
+ export function recordCommandOutcome(sessionKey, canonical, success) {
74
+ if (!canonical || !isVerificationCommand(canonical))
75
+ return;
76
+ const state = getState();
77
+ let entry = state.get(sessionKey);
78
+ if (!entry) {
79
+ entry = { failedVerifies: new Map() };
80
+ state.set(sessionKey, entry);
81
+ }
82
+ if (success) {
83
+ entry.failedVerifies.delete(canonical);
84
+ }
85
+ else {
86
+ entry.failedVerifies.set(canonical, Date.now());
87
+ }
88
+ }
89
+ export function checkPushGate(sessionKey) {
90
+ if (process.env.MUONROI_ALLOW_PUSH_ON_RED === "1")
91
+ return { blocked: false, failed: [] };
92
+ const entry = getState().get(sessionKey);
93
+ if (!entry || entry.failedVerifies.size === 0)
94
+ return { blocked: false, failed: [] };
95
+ return { blocked: true, failed: [...entry.failedVerifies.keys()] };
96
+ }
97
+ /** Message returned in place of running a blocked `git push`. */
98
+ export function pushBlockedMessage(failed) {
99
+ const list = failed.map((c) => ` • ${c}`).join("\n");
100
+ return ("BLOCKED: refusing to run `git push` — a verification command failed earlier this session and has not passed since:\n" +
101
+ `${list}\n\n` +
102
+ "Re-run the failing check until it passes (0 failures), then push. This mirrors the mandatory Pre-Push Test Gate " +
103
+ "(never push on red). If you must override for a genuine reason, set MUONROI_ALLOW_PUSH_ON_RED=1.");
104
+ }
105
+ // Repo-root paths that should essentially never be committed. Presence-based
106
+ // (fs.existsSync) so the check is O(1) and never spawns git.
107
+ const SENSITIVE_NAMES = [
108
+ ".env",
109
+ ".env.local",
110
+ ".env.production",
111
+ ".env.development",
112
+ ".muonroi-cli",
113
+ "id_rsa",
114
+ "id_ed25519",
115
+ "credentials.json",
116
+ ".npmrc",
117
+ ];
118
+ /** Sensitive paths present in `cwd` that a broad `git add` would likely sweep in. */
119
+ export function detectSensitiveStaging(cwd) {
120
+ const found = [];
121
+ for (const name of SENSITIVE_NAMES) {
122
+ try {
123
+ if (fs.existsSync(path.join(cwd, name)))
124
+ found.push(name);
125
+ }
126
+ catch {
127
+ // ignore unreadable entries — best-effort detection only
128
+ }
129
+ }
130
+ return found;
131
+ }
132
+ /** Non-blocking warning appended to a broad-stage command's output, or "". */
133
+ export function stagingWarning(cwd) {
134
+ const sensitive = detectSensitiveStaging(cwd);
135
+ if (sensitive.length === 0)
136
+ return "";
137
+ return ("\n\n[WARNING: a broad `git add`/`git commit -a` was run while these sensitive paths exist in the repo root: " +
138
+ `${sensitive.join(", ")}. Verify none were staged (git status), stage files EXPLICITLY (git add <path>), and ensure ` +
139
+ "secrets are gitignored before committing/pushing.]");
140
+ }
141
+ //# sourceMappingURL=git-safety.js.map
@@ -0,0 +1 @@
1
+ export {};