loki-mode 11.3.0 → 11.3.2

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 (242) hide show
  1. package/README.md +68 -650
  2. package/SKILL.md +5 -5
  3. package/VERSION +1 -1
  4. package/autonomy/completion-council.sh +2 -2
  5. package/autonomy/context-tracker.py +4 -4
  6. package/autonomy/council-v2.sh +1 -1
  7. package/autonomy/grill.sh +1 -1
  8. package/autonomy/lib/done-recognition.sh +1 -1
  9. package/autonomy/lib/efficiency_cost.py +8 -0
  10. package/autonomy/lib/engine10-push.sh +22 -8
  11. package/autonomy/lib/prd-enrich.sh +1 -1
  12. package/autonomy/lib/voter-agents.sh +1 -1
  13. package/autonomy/loki +161 -105
  14. package/autonomy/run.sh +30 -18
  15. package/autonomy/sandbox.sh +1 -1
  16. package/autonomy/tui.sh +6 -5
  17. package/autonomy/verify.sh +1 -1
  18. package/bin/loki +195 -59
  19. package/completions/_loki +8 -0
  20. package/completions/loki.bash +3 -3
  21. package/dashboard/__init__.py +1 -1
  22. package/dashboard/server.py +47 -45
  23. package/docs/AGENT-CHANGE-RECEIPT.md +2 -2
  24. package/docs/ARCHITECTURE.md +337 -0
  25. package/docs/BRANDING.md +124 -0
  26. package/docs/CLI-REFERENCE.md +11 -1
  27. package/docs/COMPONENTS.md +447 -0
  28. package/docs/DEPLOYMENT.md +9 -9
  29. package/docs/DOCKER.md +469 -0
  30. package/docs/INSTALLATION.md +67 -1045
  31. package/docs/KNOWN-ISSUES.md +11 -35
  32. package/docs/OTEL-COLLECTOR.md +24 -0
  33. package/docs/ROUTING-EVIDENCE.md +32 -0
  34. package/docs/TESTING.md +161 -0
  35. package/docs/air-gapped.md +1 -2
  36. package/docs/alternative-installations.md +14 -24
  37. package/docs/architecture/DECISIONS.md +333 -0
  38. package/docs/audit-logging.md +7 -7
  39. package/docs/authentication.md +3 -3
  40. package/docs/authorization.md +1 -1
  41. package/docs/certification/04-production-deployment/lab.md +6 -6
  42. package/docs/certification/04-production-deployment/lesson.md +4 -4
  43. package/docs/certification/README.md +1 -1
  44. package/docs/control-plane-container.md +2 -2
  45. package/docs/dev/real-run.md +34 -0
  46. package/docs/dev/release-checklist.md +2 -2
  47. package/docs/environment-variables.md +1 -0
  48. package/docs/exit-codes.md +3 -3
  49. package/docs/git-workflow.md +3 -3
  50. package/docs/metrics.md +37 -55
  51. package/docs/network-security.md +1 -1
  52. package/docs/siem-integration.md +5 -5
  53. package/docs/spec-first-intent.md +28 -0
  54. package/docs/v10/CLI-MODERN.md +7 -3
  55. package/docs/v10/DECISIONS.md +59 -0
  56. package/docs/v10/DEPS.md +22 -22
  57. package/docs/v10/ENGINE.md +1 -1
  58. package/docs/v10/FAILURE-CLASSES.md +212 -1
  59. package/docs/v10/FOUNDER-QUEUE.md +1 -0
  60. package/docs/v10/GUARDS.md +31 -0
  61. package/docs/v10/GUIDE.md +7 -5
  62. package/docs/v10/LEGACY-REMOVAL.md +4 -6
  63. package/docs/v10/METRICS.md +14 -2
  64. package/docs/v10/MODERNIZE.md +1 -1
  65. package/docs/v10/OPERATING-MODEL.md +4 -3
  66. package/docs/v10/PROGRESS.md +35 -0
  67. package/docs/v11/COST-HALF.md +352 -0
  68. package/docs/v11/INTOTO-AGENT-CHANGE-PREDICATE.md +77 -0
  69. package/docs/v11/MARKET-SLICES.md +315 -0
  70. package/docs/v11/MCP-2026-07-28.md +55 -0
  71. package/docs/v11/REGISTRIES.md +730 -0
  72. package/docs/v11/VERIFY-PR-SANDBOX.md +41 -0
  73. package/loki-ts/data/model-pricing.json +52 -24
  74. package/loki-ts/dist/loki.js +1185 -945
  75. package/mcp/__init__.py +1 -1
  76. package/mcp/v10_tools.py +29 -0
  77. package/package.json +9 -11
  78. package/packages/control-plane/dist/ask-tools-server.js +8 -2
  79. package/packages/control-plane/dist/server.js +15832 -12361
  80. package/packages/control-plane/drizzle/0006_token_sessions.sql +1 -0
  81. package/packages/control-plane/drizzle/meta/0006_snapshot.json +807 -0
  82. package/packages/control-plane/drizzle/meta/_journal.json +7 -0
  83. package/packages/control-plane/src/db/schema.ts +1 -0
  84. package/packages/control-plane/src/server/routes/cost.ts +4 -3
  85. package/packages/control-plane/src/server/runs.ts +3 -3
  86. package/packages/control-plane/ui/dist/assets/index-DbLzr87u.js +190 -0
  87. package/packages/control-plane/ui/dist/index.html +1 -1
  88. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  89. package/providers/opencode.sh +2 -3
  90. package/src/observability/genai-spans.js +104 -0
  91. package/src/observability/otel-bridge.js +49 -1
  92. package/src/observability/otel.js +2 -2
  93. package/src/protocols/mcp-client.js +6 -2
  94. package/src/protocols/mcp-server.js +191 -6
  95. package/src/protocols/transport/sse.js +23 -2
  96. package/tools/audit-docs.py +1 -1
  97. package/web-app/dist/assets/{AdminPage-D_e45uTw.js → AdminPage-DuRQZsF9.js} +1 -1
  98. package/web-app/dist/assets/{Avatar-T3nIpoaf.js → Avatar-7GtOQvsO.js} +1 -1
  99. package/web-app/dist/assets/{Badge-BKqb59pP.js → Badge-CYgTnIxj.js} +1 -1
  100. package/web-app/dist/assets/{Button-DjVlkh8_.js → Button-rYSyIu7X.js} +1 -1
  101. package/web-app/dist/assets/{CockpitPage-DXJgLGmh.js → CockpitPage-9adANY4o.js} +1 -1
  102. package/web-app/dist/assets/{ComparePage-BRP_eVc_.js → ComparePage-DEG2oA-R.js} +1 -1
  103. package/web-app/dist/assets/{ErrorBoundary-CLjpGLjC.js → ErrorBoundary-CwtvY4xs.js} +1 -1
  104. package/web-app/dist/assets/{EvidenceReceiptPanel-DX-vb4Xy.js → EvidenceReceiptPanel-DTT73pPB.js} +1 -1
  105. package/web-app/dist/assets/{GitHubIssuesPanel-214HVHtX.js → GitHubIssuesPanel-CpWZLz2x.js} +1 -1
  106. package/web-app/dist/assets/{GitHubPRsPanel-BEiZKc4t.js → GitHubPRsPanel-H293R7E2.js} +1 -1
  107. package/web-app/dist/assets/{HomePage-VvMHS5ga.js → HomePage-B11Imp9k.js} +2 -2
  108. package/web-app/dist/assets/{LoginPage-5WD-v7n3.js → LoginPage-BZBgx5Jr.js} +1 -1
  109. package/web-app/dist/assets/{MagicPage-DUQT_Jye.js → MagicPage-B27S4_X8.js} +1 -1
  110. package/web-app/dist/assets/{MetricsPage-lga-z79v.js → MetricsPage-CfbJWNDG.js} +1 -1
  111. package/web-app/dist/assets/{NotFoundPage-CGhBX3G1.js → NotFoundPage-Tp_4zMSm.js} +1 -1
  112. package/web-app/dist/assets/{ProjectPage-CdzF2GzZ.js → ProjectPage-CM4fgMsV.js} +3 -3
  113. package/web-app/dist/assets/{ProjectsPage-DRG64N8s.js → ProjectsPage-DP2Fvsrd.js} +1 -1
  114. package/web-app/dist/assets/{SettingsPage-5fzW9Frb.js → SettingsPage-C4G9-EAy.js} +1 -1
  115. package/web-app/dist/assets/{ShowcasePage-DrTdxO4y.js → ShowcasePage-Dj-n4dwu.js} +1 -1
  116. package/web-app/dist/assets/{SystemSettingsPage-C9Mqs6Rd.js → SystemSettingsPage-BqMLpuGt.js} +1 -1
  117. package/web-app/dist/assets/{TeamsPage-CUGPdSWo.js → TeamsPage-mIH8H96m.js} +1 -1
  118. package/web-app/dist/assets/{TemplatesPage-CNraDY0F.js → TemplatesPage-BagW-v4n.js} +1 -1
  119. package/web-app/dist/assets/{TerminalOutput-BIu_6Pea.js → TerminalOutput-CTQRHhnK.js} +1 -1
  120. package/web-app/dist/assets/{activity-Bk-kgqaf.js → activity-DNQiBDDL.js} +1 -1
  121. package/web-app/dist/assets/{bell-BXmUdmo8.js → bell-Coiwdad1.js} +1 -1
  122. package/web-app/dist/assets/{bot-CiuzXxUR.js → bot-Cou-rTgR.js} +1 -1
  123. package/web-app/dist/assets/{check-BEzjXppd.js → check-CsF-XtQp.js} +1 -1
  124. package/web-app/dist/assets/{chevron-left-AYWCXHf8.js → chevron-left-BJiy55Ht.js} +1 -1
  125. package/web-app/dist/assets/{circle-alert-BNg2uSRq.js → circle-alert-D6-pQQWq.js} +1 -1
  126. package/web-app/dist/assets/{clock-DTj0kkGT.js → clock-BOGqwgt9.js} +1 -1
  127. package/web-app/dist/assets/{cloud-x4nTDBLT.js → cloud-DsxhTtf5.js} +1 -1
  128. package/web-app/dist/assets/{code-xml-BVh_MLrk.js → code-xml-CxJDHUlE.js} +1 -1
  129. package/web-app/dist/assets/{database-DdfBaowG.js → database-BJePscl_.js} +1 -1
  130. package/web-app/dist/assets/{dollar-sign-XuYFmAOY.js → dollar-sign-6qWGAkZo.js} +1 -1
  131. package/web-app/dist/assets/{file-code-corner-D4cRnmeL.js → file-code-corner-DcOtYeDb.js} +1 -1
  132. package/web-app/dist/assets/{file-plus-CyoVlHc_.js → file-plus-WuRyLFo0.js} +1 -1
  133. package/web-app/dist/assets/{globe-Y6IK2RPW.js → globe-r-xCP4DE.js} +1 -1
  134. package/web-app/dist/assets/{hammer-BKzr9VZI.js → hammer-DTt0SsCB.js} +1 -1
  135. package/web-app/dist/assets/{index-B20cSx5G.js → index-BkbXyH8y.js} +3 -3
  136. package/web-app/dist/assets/{layers-DebNOhGS.js → layers-BvZhvSMY.js} +1 -1
  137. package/web-app/dist/assets/{loader-circle-DyGbrEgd.js → loader-circle-3gosTei9.js} +1 -1
  138. package/web-app/dist/assets/{lock-BKPpyt20.js → lock-CZZMxP0U.js} +1 -1
  139. package/web-app/dist/assets/{package-egF5Wi4y.js → package-C5A8CkJr.js} +1 -1
  140. package/web-app/dist/assets/{plus-OiEgKIc-.js → plus-Btv7tTIF.js} +1 -1
  141. package/web-app/dist/assets/{refresh-cw-CzKvrONq.js → refresh-cw-DsWLXgs_.js} +1 -1
  142. package/web-app/dist/assets/{rotate-ccw-Bu2mII0p.js → rotate-ccw-DOVDjway.js} +1 -1
  143. package/web-app/dist/assets/{scroll-text-CSkiyax8.js → scroll-text-smySzlrX.js} +1 -1
  144. package/web-app/dist/assets/{server-DlPthJFw.js → server-b5PHsqAU.js} +1 -1
  145. package/web-app/dist/assets/{shield-alert-CbpHWQQX.js → shield-alert-BD7FDlh-.js} +1 -1
  146. package/web-app/dist/assets/{thumbs-up-DgGpZe-Q.js → thumbs-up-CAsFzbyE.js} +1 -1
  147. package/web-app/dist/assets/{trash-2-BV3GlCB-.js → trash-2-BJ35qHgl.js} +1 -1
  148. package/web-app/dist/assets/{trending-down-Y4TJtXhD.js → trending-down-Ozrd26cZ.js} +1 -1
  149. package/web-app/dist/assets/{trending-up-DsDNoHZY.js → trending-up-BhnMcM32.js} +1 -1
  150. package/web-app/dist/assets/{upload-gkThrc_I.js → upload-JVHfdwoA.js} +1 -1
  151. package/web-app/dist/assets/{usePolling-jpNSeNAe.js → usePolling-CTzbDV-e.js} +1 -1
  152. package/web-app/dist/assets/{user-3Zez1WOK.js → user-Bks3-GYv.js} +1 -1
  153. package/web-app/dist/index.html +1 -1
  154. package/docs/ARCHITECTURE-OVERVIEW.md +0 -209
  155. package/docs/AUTONOMI-ECOSYSTEM.md +0 -107
  156. package/docs/COCKPIT-SPEC.md +0 -72
  157. package/docs/COCKPIT-UX-MANDATE.md +0 -53
  158. package/docs/COMPETITIVE-ANALYSIS.md +0 -303
  159. package/docs/COMPETITIVE-NEXT-10.md +0 -316
  160. package/docs/COMPETITIVE-RALPH-ZEROSHOT-2026-07-08.md +0 -81
  161. package/docs/DASHBOARD-9.12-EVIDENCE.md +0 -97
  162. package/docs/DASHBOARD-ARCHITECTURE.md +0 -425
  163. package/docs/DASHBOARD_V2_CHECKLIST.md +0 -243
  164. package/docs/DEMOS.md +0 -63
  165. package/docs/ENTERPRISE-IDENTITY-ROADMAP.md +0 -206
  166. package/docs/EVIDENCE-RECEIPT-BUILD-APPLICABILITY.md +0 -79
  167. package/docs/FEAT-PRDREUSE-DOCKER-PLAN.md +0 -144
  168. package/docs/GATE-FAILURE-TRIAGE.md +0 -254
  169. package/docs/HANDOFF-2026-08-03.md +0 -439
  170. package/docs/LOOP-CANDIDATE-PROPOSAL-v1.md +0 -167
  171. package/docs/LOOP-HARNESS-AUDIT.md +0 -684
  172. package/docs/MERGE-DEDUP-MAP.md +0 -420
  173. package/docs/MERGE-ROUTE-MAP.md +0 -139
  174. package/docs/MODEL-REGISTRY-PLAN.md +0 -155
  175. package/docs/OUTCOME-FRONTIER.md +0 -536
  176. package/docs/P2-SPEC-ROBUSTNESS-PLAN.md +0 -192
  177. package/docs/PLANS-INDEX.md +0 -32
  178. package/docs/PRD-REUSE-DONE-RECOGNITION-PLAN.md +0 -536
  179. package/docs/PROMPT-ABLATION-RESULT.md +0 -97
  180. package/docs/R4-TRUST-TRAJECTORY-DESIGN.md +0 -127
  181. package/docs/R5-AUTO-WIKI-DESIGN.md +0 -137
  182. package/docs/R6-ROLLBACK-CHECKPOINT-PLAN.md +0 -107
  183. package/docs/R7-ZERO-CONFIG-FIRST-RUN-PLAN.md +0 -137
  184. package/docs/R8-SHAREABLE-TEAM-ASSETS-PLAN.md +0 -129
  185. package/docs/R9-OPEN-CORE-HOOKS-PLAN.md +0 -113
  186. package/docs/RARV-C-100X-PLAN.md +0 -286
  187. package/docs/RARV-C-LOOP-EFFICIENCY-PLAN.md +0 -470
  188. package/docs/SHAREABLE-PROOF-PLAN.md +0 -194
  189. package/docs/SONNET5-DEFAULT-PLAN.md +0 -157
  190. package/docs/SPEED-AND-FIRST-PASS-PLAN.md +0 -281
  191. package/docs/STALE-STATE-AUDIT.md +0 -174
  192. package/docs/SYNERGY-TASKS.md +0 -143
  193. package/docs/TASKLIST-ACCURACY-PLAN.md +0 -355
  194. package/docs/TOP-100-BACKLOG.md +0 -159
  195. package/docs/V8-ACCEPTANCE-TRIAGE-2026-07-24.md +0 -114
  196. package/docs/V8-COMPLEXITY-AUDIT-2026-07-24.md +0 -45
  197. package/docs/V8-MAJOR-RELEASE-PLAN.md +0 -137
  198. package/docs/V8-OVERNIGHT-PLAN-2026-07-25.md +0 -82
  199. package/docs/V8-SDK-RESEARCH-RAW.md +0 -129
  200. package/docs/WELCOME-OPENER-PLAN.md +0 -106
  201. package/docs/adoption-baseline-2026-07-31.md +0 -84
  202. package/docs/architecture/bmad-integration-epic.md +0 -271
  203. package/docs/architecture/bmad-integration-review.md +0 -86
  204. package/docs/architecture/bmad-integration-validation.md +0 -249
  205. package/docs/architecture/bmad-loki-voice-agent-council-analysis.md +0 -61
  206. package/docs/auto-claude-comparison.md +0 -250
  207. package/docs/bug-fixes/agent-01-cli-fixes.md +0 -101
  208. package/docs/bug-fixes/agent-02-purplelab-fixes.md +0 -88
  209. package/docs/bug-fixes/agent-03-dashboard-fixes.md +0 -119
  210. package/docs/bug-fixes/agent-04-memory-fixes.md +0 -105
  211. package/docs/bug-fixes/agent-05-provider-fixes.md +0 -86
  212. package/docs/bug-fixes/agent-06-integration-fixes.md +0 -101
  213. package/docs/bug-fixes/agent-07-dash-run-fixes.md +0 -101
  214. package/docs/bug-fixes/agent-08-docker-fixes.md +0 -164
  215. package/docs/bug-fixes/agent-09-e2e-build-fixes.md +0 -69
  216. package/docs/bug-fixes/agent-10-e2e-fullstack-fixes.md +0 -102
  217. package/docs/bug-fixes/agent-11-e2e-session-fixes.md +0 -70
  218. package/docs/bug-fixes/agent-12-scenario-fixes.md +0 -120
  219. package/docs/bug-fixes/agent-13-enterprise-fixes.md +0 -143
  220. package/docs/bug-fixes/agent-14-uat-newuser-fixes.md +0 -88
  221. package/docs/bug-fixes/agent-15-uat-poweruser-fixes.md +0 -132
  222. package/docs/bug-fixes/agent-19-code-review.md +0 -316
  223. package/docs/bug-fixes/agent-20-architecture-review.md +0 -331
  224. package/docs/competitive/bolt-new-analysis.md +0 -579
  225. package/docs/competitive/emergence-others-analysis.md +0 -605
  226. package/docs/competitive/replit-lovable-analysis.md +0 -622
  227. package/docs/dashboard-guide.md +0 -487
  228. package/docs/enterprise/migration.md +0 -265
  229. package/docs/history/one-run-audit.md +0 -304
  230. package/docs/plans/FORGE-AUTONOMOUS-QUEUE.md +0 -312
  231. package/docs/plans/ULTRAPLAN-FORGE-BAAS.md +0 -612
  232. package/docs/plans/UT2-6-LSP-DIAGNOSTIC-BROADCAST.md +0 -394
  233. package/docs/prd-purple-lab-ide.md +0 -152
  234. package/docs/prd-purple-lab-platform-v2.md +0 -223
  235. package/docs/prd-purple-lab-platform.md +0 -319
  236. package/docs/retrospectives/v7.5.15-fleet-postmortem.md +0 -325
  237. package/docs/retrospectives/v7.5.15-honesty-audit.md +0 -136
  238. package/docs/retrospectives/v7.5.15-llm-failure-modes.md +0 -49
  239. package/docs/show-hn-post.md +0 -46
  240. package/docs/test-scenarios/enterprise-scenarios.md +0 -732
  241. package/docs/thick2thin.md +0 -173
  242. package/packages/control-plane/ui/dist/assets/index-DcBJXjCJ.js +0 -190
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Loki Mode</title>
7
- <script type="module" crossorigin src="./assets/index-DcBJXjCJ.js"></script>
7
+ <script type="module" crossorigin src="./assets/index-DbLzr87u.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="./assets/index-CxZWgwie.css">
9
9
  </head>
10
10
  <body>
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "11.3.0",
5
+ "version": "11.3.2",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",
@@ -21,9 +21,8 @@
21
21
  # v0.86.0 from 2025-08-09, and Roo Code is archived, Continue read-only --
22
22
  # so opencode, not aider, is the durable cheap-model route.)
23
23
  #
24
- # Cost context (SWE-bench uniform scaffold, mini-swe-agent v2.0.0, 2026-02-17):
25
- # MiniMax M2.5 resolved 75.8 at $36.64 total vs Claude Opus 4.6 at 75.6 for
26
- # $275.76 -- an open model matching frontier accuracy at ~7.5x lower cost.
24
+ # Cost context: see docs/ROUTING-EVIDENCE.md (measurement source, date and
25
+ # caveats for the MiniMax M2.5 vs Claude Opus 4.6 cost-parity figures).
27
26
  #
28
27
  # CLI SURFACE (verified against the installed binary, v1.17.9, not assumed):
29
28
  # opencode run [message..] non-interactive run
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Pure mapper: engine10 events.jsonl envelopes -> gen_ai span descriptors.
5
+ * No I/O, no network, no clock. Attributes are built from a fixed whitelist of
6
+ * event fields, so prompt or completion content and secrets can never reach a span.
7
+ */
8
+
9
+ // OpenTelemetry GenAI semantic conventions version these attribute names follow.
10
+ const GENAI_SEMCONV_VERSION = '1.37.0';
11
+
12
+ const AGENT_NAME = 'loki';
13
+
14
+ function toNs(ts) {
15
+ const ms = Date.parse(ts);
16
+ return Number.isFinite(ms) ? BigInt(ms) * 1000000n : null;
17
+ }
18
+
19
+ function isNum(x) {
20
+ return typeof x === 'number' && Number.isFinite(x);
21
+ }
22
+
23
+ // Unknown usage is absent, never 0 (mirrors engine10/cost.ts): an unmetered or
24
+ // unmeasured cost event, or an all-zero usage record, carries no token attributes.
25
+ function usageAttrs(d) {
26
+ if (d.source === 'cli-invoker-unmetered' || d.source === 'not measured') return {};
27
+ if (!isNum(d.input_tokens) || !isNum(d.output_tokens)) return {};
28
+ if (d.input_tokens === 0 && d.output_tokens === 0) return {};
29
+ return { 'gen_ai.usage.input_tokens': d.input_tokens, 'gen_ai.usage.output_tokens': d.output_tokens };
30
+ }
31
+
32
+ /**
33
+ * @param {Array<{seq:number,ts:string,run:string,type:string,stage:string|null,data:object}>} events
34
+ * @returns {Array<{key:string,parentKey:string|null,name:string,startNs:bigint,endNs:bigint,status:'ok'|'error'|'unset',attributes:object}>}
35
+ */
36
+ function mapRunEvents(events) {
37
+ const list = (Array.isArray(events) ? events : []).filter((e) => e && typeof e.type === 'string' && toNs(e.ts) !== null);
38
+ if (list.length === 0) return [];
39
+ const started = list.find((e) => e.type === 'run.started');
40
+ const root = started || list[0];
41
+ const last = list[list.length - 1];
42
+ const done = list.find((e) => e.type === 'run.completed');
43
+ const provider = started && typeof started.data.provider === 'string' ? started.data.provider : null;
44
+ const runModel = started && typeof started.data.model === 'string' ? started.data.model : null;
45
+
46
+ const base = { 'gen_ai.agent.name': AGENT_NAME };
47
+ if (provider) base['gen_ai.provider.name'] = provider;
48
+ const rootKey = 'run:' + root.run;
49
+ const spans = [{
50
+ key: rootKey, parentKey: null, name: 'invoke_agent ' + AGENT_NAME,
51
+ startNs: toNs(root.ts), endNs: toNs((done || last).ts),
52
+ status: done ? (done.data.verdict === 'FAILED' ? 'error' : 'ok') : 'unset',
53
+ attributes: Object.assign({ 'gen_ai.operation.name': 'invoke_agent' }, base, runModel ? { 'gen_ai.request.model': runModel } : {}),
54
+ }];
55
+
56
+ const open = new Map();
57
+ const costs = [];
58
+ for (const e of list) {
59
+ const d = e.data || {};
60
+ if (e.type === 'stage.started' && e.stage) {
61
+ open.set(e.stage, e);
62
+ } else if ((e.type === 'stage.completed' || e.type === 'stage.failed' || e.type === 'stage.skipped') && e.stage) {
63
+ const s = open.get(e.stage);
64
+ open.delete(e.stage);
65
+ spans.push({
66
+ key: 'stage:' + e.stage, parentKey: rootKey, name: 'stage ' + e.stage,
67
+ startNs: toNs((s || e).ts), endNs: toNs(e.ts), status: e.type === 'stage.failed' ? 'error' : 'ok',
68
+ attributes: { 'gen_ai.operation.name': 'invoke_agent', 'loki.stage': e.stage },
69
+ });
70
+ } else if (e.type === 'cost') {
71
+ costs.push(e);
72
+ } else if (e.type.indexOf('tool.') === 0 && typeof d.name === 'string' && d.name) {
73
+ const t = toNs(e.ts);
74
+ spans.push({
75
+ key: 'tool:' + e.seq, parentKey: e.stage ? 'stage:' + e.stage : rootKey, name: 'execute_tool ' + d.name,
76
+ startNs: t, endNs: t, status: e.type === 'tool.failed' ? 'error' : 'ok',
77
+ attributes: { 'gen_ai.operation.name': 'execute_tool', 'gen_ai.tool.name': d.name },
78
+ });
79
+ }
80
+ }
81
+ // Stages still open at the end of the log end at the last event.
82
+ for (const [stage, s] of open) {
83
+ spans.push({
84
+ key: 'stage:' + stage, parentKey: rootKey, name: 'stage ' + stage,
85
+ startNs: toNs(s.ts), endNs: toNs(last.ts), status: 'unset',
86
+ attributes: { 'gen_ai.operation.name': 'invoke_agent', 'loki.stage': stage },
87
+ });
88
+ }
89
+ for (const e of costs) {
90
+ const attrs = usageAttrs(e.data || {});
91
+ const stageSpan = spans.find((s) => s.key === 'stage:' + e.stage);
92
+ const targets = stageSpan ? [stageSpan, spans[0]] : [spans[0]];
93
+ for (const t of targets) {
94
+ if (t === spans[0] && typeof e.data.model === 'string' && e.data.model) t.attributes['gen_ai.request.model'] = e.data.model;
95
+ for (const [k, v] of Object.entries(attrs)) t.attributes[k] = (t.attributes[k] || 0) + v;
96
+ }
97
+ if (stageSpan && typeof e.data.model === 'string' && e.data.model) stageSpan.attributes['gen_ai.request.model'] = e.data.model;
98
+ }
99
+ // Parents before children so a replaying exporter can link them.
100
+ const rank = (x) => (x.parentKey === null ? 0 : x.key.indexOf('stage:') === 0 ? 1 : 2);
101
+ return spans.map((x, i) => ({ x, i })).sort((p, q) => rank(p.x) - rank(q.x) || p.i - q.i).map((o) => o.x);
102
+ }
103
+
104
+ module.exports = { GENAI_SEMCONV_VERSION, mapRunEvents };
@@ -195,6 +195,52 @@ function processEventFile(filepath, tracer, traceId, otelRef) {
195
195
  }
196
196
  }
197
197
 
198
+ // ---------------------------------------------------------------------------
199
+ // Engine10 gen_ai spans (LOKI_OTEL_GENAI=1): completed runs under .loki/runs/<id>/events.jsonl
200
+ // ---------------------------------------------------------------------------
201
+
202
+ const exportedRuns = new Set();
203
+
204
+ function genaiEnabled() {
205
+ return /^(1|true|yes|on)$/i.test(String(process.env.LOKI_OTEL_GENAI || '').trim());
206
+ }
207
+
208
+ /** Emit the span tree for one run's envelopes. Returns the number of spans ended. */
209
+ function exportEngine10Run(events, tracer, traceId, otelRef, rootParentSpanId) {
210
+ const spans = require('./genai-spans').mapRunEvents(events);
211
+ const live = new Map();
212
+ for (const d of spans) {
213
+ const parent = d.parentKey ? live.get(d.parentKey) : null;
214
+ const span = tracer.startSpan(d.name, { traceId: traceId, parentSpanId: parent ? parent.spanId : (d.parentKey ? undefined : rootParentSpanId), attributes: Object.assign({}, d.attributes) });
215
+ span.startTimeUnixNano = String(d.startNs);
216
+ live.set(d.key, span);
217
+ if (d.status === 'ok') span.setStatus(otelRef.SpanStatusCode.OK);
218
+ else if (d.status === 'error') span.setStatus(otelRef.SpanStatusCode.ERROR);
219
+ span.end(d.endNs);
220
+ }
221
+ return spans.length;
222
+ }
223
+
224
+ function scanEngine10Runs(tracer, traceId, otelRef) {
225
+ if (!genaiEnabled()) return;
226
+ const runsDir = path.join(process.cwd(), lokiDir, 'runs');
227
+ try {
228
+ for (const id of fs.readdirSync(runsDir)) {
229
+ if (exportedRuns.has(id)) continue;
230
+ const f = path.join(runsDir, id, 'events.jsonl');
231
+ if (!fs.existsSync(f)) continue;
232
+ const events = [];
233
+ for (const line of fs.readFileSync(f, 'utf8').split('\n')) {
234
+ try { if (line.trim()) events.push(JSON.parse(line)); } catch (e) { /* torn line */ }
235
+ }
236
+ if (!events.some((e) => e && e.type === 'run.completed')) continue; // export complete trees only
237
+ exportedRuns.add(id);
238
+ const envParent = /^[0-9a-f]{16}$/.test(process.env.LOKI_PARENT_SPAN_ID || '') ? process.env.LOKI_PARENT_SPAN_ID : undefined; // MCP-D: parent from an MCP task's traceparent
239
+ exportEngine10Run(events, tracer, traceId, otelRef, envParent);
240
+ }
241
+ } catch (e) { /* no runs dir */ }
242
+ }
243
+
198
244
  // ---------------------------------------------------------------------------
199
245
  // Polling loop
200
246
  // ---------------------------------------------------------------------------
@@ -229,7 +275,7 @@ function start() {
229
275
 
230
276
  // Bound versions that close over initialized dependencies
231
277
  const boundProcessEventFile = (filepath) => processEventFile(filepath, tracer, traceId, otelMod);
232
- const boundScanPendingEvents = () => scanPendingEvents(tracer, traceId, otelMod);
278
+ const boundScanPendingEvents = () => { scanPendingEvents(tracer, traceId, otelMod); scanEngine10Runs(tracer, traceId, otelMod); };
233
279
 
234
280
  pollInterval = setInterval(boundScanPendingEvents, POLL_INTERVAL_MS);
235
281
 
@@ -275,6 +321,8 @@ function start() {
275
321
  module.exports = {
276
322
  start,
277
323
  processEventFile,
324
+ exportEngine10Run,
325
+ scanEngine10Runs,
278
326
  activeSpans,
279
327
  _getLastProcessedFile: function() { return lastProcessedFile; },
280
328
  _resetState: function() { lastProcessedFile = ''; },
@@ -95,10 +95,10 @@ class Span {
95
95
  return this;
96
96
  }
97
97
 
98
- end() {
98
+ end(endNanos) {
99
99
  if (this._ended) return;
100
100
  this._ended = true;
101
- this.endTimeUnixNano = nowNanos();
101
+ this.endTimeUnixNano = endNanos !== undefined ? String(endNanos) : nowNanos(); // endNanos: replayed spans (gen_ai bridge) keep event time
102
102
  // Register with the exporter
103
103
  if (_activeExporter) {
104
104
  _activeExporter.addSpan(this);
@@ -70,6 +70,7 @@ class MCPClient extends EventEmitter {
70
70
  this._connected = false;
71
71
  this._connectingPromise = null;
72
72
  this._serverInfo = null;
73
+ this._protocolVersion = null;
73
74
  this._serverCapabilities = null;
74
75
  this._tools = null;
75
76
 
@@ -84,6 +85,7 @@ class MCPClient extends EventEmitter {
84
85
  get name() { return this._name; }
85
86
  get connected() { return this._connected; }
86
87
  get serverInfo() { return this._serverInfo; }
88
+ get protocolVersion() { return this._protocolVersion; }
87
89
  get tools() { return this._tools; }
88
90
 
89
91
  async connect() {
@@ -102,13 +104,14 @@ class MCPClient extends EventEmitter {
102
104
 
103
105
  const initResult = await this._sendRequest('initialize', {
104
106
  clientInfo: { name: 'loki-mode-client', version: '1.0.0' },
105
- protocolVersion: '2024-11-05'
107
+ protocolVersion: '2025-11-25'
106
108
  });
107
109
 
108
110
  this._serverInfo = initResult.serverInfo || null;
111
+ this._protocolVersion = initResult.protocolVersion || null;
109
112
  this._serverCapabilities = initResult.capabilities || null;
110
113
 
111
- this._sendNotification('initialized', {});
114
+ this._sendNotification('notifications/initialized', {});
112
115
 
113
116
  const toolsResult = await this._sendRequest('tools/list', {});
114
117
  this._tools = toolsResult.tools || [];
@@ -142,6 +145,7 @@ class MCPClient extends EventEmitter {
142
145
  this._connected = false;
143
146
  this._tools = null;
144
147
  this._serverInfo = null;
148
+ this._protocolVersion = null;
145
149
 
146
150
  for (const [, pending] of this._pendingRequests) {
147
151
  clearTimeout(pending.timer);
@@ -20,6 +20,8 @@
20
20
 
21
21
  const path = require('path');
22
22
  const fs = require('fs');
23
+ const crypto = require('crypto');
24
+ const { spawn } = require('child_process');
23
25
 
24
26
  // ---------------------------------------------------------------------------
25
27
  // Lazy tool/resource loading
@@ -93,11 +95,158 @@ function getServerInfo() {
93
95
 
94
96
  return {
95
97
  name: 'loki-mode',
96
- version: version,
97
- protocolVersion: '2024-11-05'
98
+ version: version
98
99
  };
99
100
  }
100
101
 
102
+ // Oldest to newest; the last entry is the fallback offered to unknown clients.
103
+ const SUPPORTED_PROTOCOL_VERSIONS = ['2024-11-05', '2025-03-26', '2025-06-18', '2025-11-25'];
104
+
105
+ // MCP-C: 2026-07-28 (stateless profile) is advertised only with LOKI_MCP_2026_07=1. It is never the initialize fallback,
106
+ // because that profile removes the handshake and a legacy client must keep getting a stateful version.
107
+ const STATELESS_VERSION = '2026-07-28';
108
+ const META_PROTOCOL_VERSION = 'io.modelcontextprotocol/protocolVersion';
109
+
110
+ function advertisedProtocolVersions() {
111
+ return process.env.LOKI_MCP_2026_07 === '1' ? SUPPORTED_PROTOCOL_VERSIONS.concat(STATELESS_VERSION) : SUPPORTED_PROTOCOL_VERSIONS.slice();
112
+ }
113
+
114
+ function negotiateProtocolVersion(params) {
115
+ const requested = params && typeof params.protocolVersion === 'string' ? params.protocolVersion : null;
116
+ if (requested && advertisedProtocolVersions().includes(requested)) return requested;
117
+ return SUPPORTED_PROTOCOL_VERSIONS[SUPPORTED_PROTOCOL_VERSIONS.length - 1];
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // MCP-D: Tasks extension (io.modelcontextprotocol/tasks), LOKI_MCP_TASKS=1
122
+ // ---------------------------------------------------------------------------
123
+ // A task wraps exactly two existing tools, loki_v10_verify and loki_v10_run, by running the read-only adapter in
124
+ // mcp/v10_tools.py (argument list, no shell). It never computes or edits a verdict: the task result is the adapter's
125
+ // stdout, byte for byte. Cancel kills only the child this server spawned, by its recorded pid. State is in memory.
126
+
127
+ const TASK_TOOLS = {
128
+ loki_v10_verify: { cmd: 'verify', keys: ['receipt_path', 'repo_path'], required: [['receipt_path', 'repo_path']] },
129
+ loki_v10_run: { cmd: 'run', keys: ['ref', 'repo_path'], required: [['ref'], ['repo_path']] }
130
+ };
131
+ const TASK_MAX = 64;
132
+ const TASK_TIMEOUT_MS = 300000;
133
+ const TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/;
134
+ // Same list the adapter's _env() drops for the direct tool (mcp/v10_tools.py _DROPPED_ENV): one mechanism, so a task
135
+ // sees the same environment as the direct call. MCP_AUTH_TOKEN is the server's own bearer and is never forwarded.
136
+ const TASK_DROPPED_ENV = ['LOKI_CONTROL_TOKEN', 'SLACK_BOT_TOKEN', 'SLACK_SIGNING_SECRET', 'SLACK_WEBHOOK_URL', 'MCP_AUTH_TOKEN'];
137
+
138
+ const tasks = new Map();
139
+ let _taskSpawner = null;
140
+
141
+ function tasksEnabled() {
142
+ return process.env.LOKI_MCP_TASKS === '1';
143
+ }
144
+
145
+ function _setTaskSpawnerForTests(fn) {
146
+ _taskSpawner = fn;
147
+ }
148
+
149
+ function _taskPidForTests(taskId) {
150
+ const t = tasks.get(taskId);
151
+ return t ? t.pid : undefined;
152
+ }
153
+
154
+ /** W3C traceparent -> { traceId, parentSpanId }, or null for anything malformed or all-zero. */
155
+ function parseTraceparent(value) {
156
+ if (typeof value !== 'string') return null;
157
+ const m = TRACEPARENT_RE.exec(value);
158
+ if (!m || /^0+$/.test(m[1]) || /^0+$/.test(m[2])) return null;
159
+ return { traceId: m[1], parentSpanId: m[2] };
160
+ }
161
+
162
+ function taskChildEnv(trace) {
163
+ const env = {};
164
+ for (const [k, v] of Object.entries(process.env)) {
165
+ if (typeof v === 'string' && !TASK_DROPPED_ENV.includes(k)) env[k] = v;
166
+ }
167
+ delete env.LOKI_PARENT_SPAN_ID;
168
+ env.LOKI_NO_BROWSER = '1';
169
+ if (trace) {
170
+ env.LOKI_TRACE_ID = trace.traceId;
171
+ env.LOKI_PARENT_SPAN_ID = trace.parentSpanId;
172
+ }
173
+ return env;
174
+ }
175
+
176
+ function taskArgs(tool, args) {
177
+ const spec = TASK_TOOLS[Object.prototype.hasOwnProperty.call(TASK_TOOLS, tool) ? tool : ''];
178
+ if (!spec) return { error: 'tool cannot run as a task: ' + String(tool) };
179
+ if (!args || typeof args !== 'object' || Array.isArray(args)) return { error: 'arguments must be an object' };
180
+ for (const k of Object.keys(args)) {
181
+ if (!spec.keys.includes(k)) return { error: 'unknown argument: ' + k };
182
+ }
183
+ for (const k of spec.keys) {
184
+ const v = args[k];
185
+ if (v === undefined) continue;
186
+ if (typeof v !== 'string' || v.length > 4096 || v.includes('\0') || v.startsWith('-')) return { error: 'invalid ' + k };
187
+ }
188
+ for (const anyOf of spec.required) {
189
+ if (!anyOf.some((k) => typeof args[k] === 'string' && args[k] !== '')) return { error: anyOf.join(' or ') + ' is required' };
190
+ }
191
+ return { spec, argv: spec.keys.map((k) => (typeof args[k] === 'string' ? args[k] : '')) };
192
+ }
193
+
194
+ function taskView(t) {
195
+ return { taskId: t.id, status: t.status, tool: t.tool };
196
+ }
197
+
198
+ function taskCreate(params, id) {
199
+ const p = params || {};
200
+ const a = taskArgs(p.tool, p.arguments);
201
+ if (a.error) return makeError(-32602, a.error, id);
202
+ const trace = parseTraceparent(p._meta && p._meta.traceparent);
203
+ while (tasks.size >= TASK_MAX) {
204
+ const old = Array.from(tasks.values()).find((t) => t.status !== 'working');
205
+ if (!old) return makeError(-32603, 'too many running tasks', id);
206
+ tasks.delete(old.id);
207
+ }
208
+ const adapter = path.resolve(__dirname, '..', '..', 'mcp', 'v10_tools.py');
209
+ const cmd = process.env.LOKI_PYTHON || 'python3';
210
+ const args = ['-I', adapter, a.spec.cmd].concat(a.argv);
211
+ const spawner = _taskSpawner || ((c, av, o) => spawn(c, av, { env: o.env, stdio: ['ignore', 'pipe', 'pipe'] }));
212
+ let child;
213
+ try {
214
+ child = spawner(cmd, args, { env: taskChildEnv(trace) });
215
+ } catch (err) {
216
+ return makeError(-32603, 'could not start task', id);
217
+ }
218
+ const t = { id: crypto.randomUUID(), tool: p.tool, status: 'working', pid: child.pid, stdout: '', text: null, child: child };
219
+ tasks.set(t.id, t);
220
+ if (child.stdout) child.stdout.on('data', (d) => { if (t.stdout.length < 1048576) t.stdout += d; });
221
+ const timer = setTimeout(() => { if (t.status === 'working') { t.status = 'failed'; t.text = 'task timed out'; try { child.kill('SIGTERM'); } catch (e) { /* gone */ } } }, TASK_TIMEOUT_MS);
222
+ timer.unref();
223
+ child.on('error', () => { clearTimeout(timer); if (t.status === 'working') { t.status = 'failed'; t.text = 'adapter could not run'; } });
224
+ child.on('close', (code) => {
225
+ clearTimeout(timer);
226
+ if (t.status !== 'working') return;
227
+ if (code === 0 && t.stdout.trim()) { t.status = 'completed'; t.text = t.stdout.replace(/\n$/, ''); } else { t.status = 'failed'; t.text = 'adapter exited ' + String(code); }
228
+ });
229
+ return makeResult({ task: taskView(t) }, id);
230
+ }
231
+
232
+ function taskDispatch(method, params, id) {
233
+ if (method === 'tasks/create') return taskCreate(params, id);
234
+ const t = params && typeof params.taskId === 'string' ? tasks.get(params.taskId) : undefined;
235
+ if (!t) return makeError(-32602, 'Unknown taskId', id);
236
+ if (method === 'tasks/get') return makeResult({ task: taskView(t) }, id);
237
+ if (method === 'tasks/result') {
238
+ if (t.status === 'working') return makeError(-32602, 'Task not finished', id);
239
+ if (t.status === 'cancelled') return makeError(-32602, 'Task was cancelled', id);
240
+ return makeResult({ content: [{ type: 'text', text: t.text }], isError: t.status === 'failed' }, id);
241
+ }
242
+ // tasks/cancel: signal only the child this server recorded
243
+ if (t.status === 'working') {
244
+ t.status = 'cancelled';
245
+ try { t.child.kill('SIGTERM'); } catch (e) { /* already gone */ }
246
+ }
247
+ return makeResult({ task: taskView(t) }, id);
248
+ }
249
+
101
250
  function getCapabilities() {
102
251
  return {
103
252
  tools: {},
@@ -109,7 +258,7 @@ function getCapabilities() {
109
258
  // JSON-RPC 2.0 request handler
110
259
  // ---------------------------------------------------------------------------
111
260
 
112
- function handleRequest(request) {
261
+ function handleRequest(request, ctx) {
113
262
  // Validate basic JSON-RPC structure
114
263
  if (!request || typeof request !== 'object') {
115
264
  return makeError(-32600, 'Invalid request', null);
@@ -129,10 +278,17 @@ function handleRequest(request) {
129
278
  const isNotification = id === undefined || id === null;
130
279
 
131
280
  // Auth check for tool/resource calls
132
- if (method === 'tools/call' || method === 'resources/read') {
281
+ if (method === 'tools/call' || method === 'resources/read' || (tasksEnabled() && method.startsWith('tasks/'))) {
133
282
  const auth = getAuth();
283
+ const overHttp = !!(ctx && ctx.transport === 'http') && method.startsWith('tasks/');
284
+ // Tasks spawn processes: over HTTP a token is mandatory even when general auth is off (fail closed).
285
+ if (overHttp && !auth.enabled) {
286
+ if (isNotification) return null;
287
+ return makeError(-32001, 'Tasks over HTTP require an auth token (set MCP_AUTH_TOKEN)', id);
288
+ }
134
289
  if (auth.enabled) {
135
- const validation = auth.validate(request);
290
+ let validation = auth.validate(request);
291
+ if (overHttp && !validation.valid && ctx.authorization) validation = auth.validateHeader(ctx.authorization);
136
292
  if (!validation.valid) {
137
293
  if (isNotification) return null;
138
294
  return makeError(-32001, validation.error || 'Unauthorized', id);
@@ -140,17 +296,46 @@ function handleRequest(request) {
140
296
  }
141
297
  }
142
298
 
299
+ // Stateless profile: a request may carry its protocol version in _meta. No session is created or stored.
300
+ const metaVersion = params && params._meta && typeof params._meta === 'object' ? params._meta[META_PROTOCOL_VERSION] : undefined;
301
+ if (metaVersion !== undefined && !advertisedProtocolVersions().includes(metaVersion)) {
302
+ if (isNotification) return null;
303
+ const err = makeError(-32022, 'Unsupported protocol version: ' + String(metaVersion), id);
304
+ err.error.data = { supported: advertisedProtocolVersions() };
305
+ return err;
306
+ }
307
+
143
308
  let result;
144
309
 
145
310
  switch (method) {
146
311
  case 'initialize':
147
312
  result = {
313
+ protocolVersion: negotiateProtocolVersion(params),
314
+ serverInfo: getServerInfo(),
315
+ capabilities: getCapabilities()
316
+ };
317
+ break;
318
+
319
+ case 'tasks/create':
320
+ case 'tasks/get':
321
+ case 'tasks/result':
322
+ case 'tasks/cancel':
323
+ if (!tasksEnabled()) {
324
+ if (isNotification) return null;
325
+ return makeError(-32601, 'Method not found: ' + method, id);
326
+ }
327
+ return taskDispatch(method, params, id);
328
+
329
+ case 'server/discover':
330
+ result = {
331
+ supportedVersions: advertisedProtocolVersions(),
148
332
  serverInfo: getServerInfo(),
149
333
  capabilities: getCapabilities()
150
334
  };
151
335
  break;
152
336
 
153
337
  case 'initialized':
338
+ case 'notifications/initialized':
154
339
  // Client acknowledgment -- no response needed
155
340
  return isNotification ? null : makeResult({}, id);
156
341
 
@@ -359,7 +544,7 @@ function main() {
359
544
  }
360
545
 
361
546
  // Export for testing
362
- module.exports = { handleRequest, getTools, getResources, getAuth, getServerInfo, main };
547
+ module.exports = { parseTraceparent, _setTaskSpawnerForTests, _taskPidForTests, SUPPORTED_PROTOCOL_VERSIONS, handleRequest, getTools, getResources, getAuth, getServerInfo, main };
363
548
 
364
549
  // Run if executed directly
365
550
  if (require.main === module) {
@@ -14,6 +14,12 @@ const http = require('http');
14
14
  // Maximum allowed POST body size (10 MB)
15
15
  const MAX_BODY_BYTES = 10 * 1024 * 1024;
16
16
 
17
+ const LOOPBACK_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/i;
18
+
19
+ function _isTaskMethod(r) {
20
+ return !!r && typeof r === 'object' && typeof r.method === 'string' && r.method.startsWith('tasks/');
21
+ }
22
+
17
23
  class SSETransport {
18
24
  constructor(handler, options) {
19
25
  this._handler = handler;
@@ -151,9 +157,24 @@ class SSETransport {
151
157
  return;
152
158
  }
153
159
 
160
+ // Task methods spawn processes. A text/plain cross-origin POST is a CORS simple request (no preflight), so
161
+ // require a JSON Content-Type and a loopback (or absent) Origin before the handler ever sees them.
162
+ const ctx = { transport: 'http', authorization: req.headers.authorization };
163
+ const touchesTasks = Array.isArray(request) ? request.some(_isTaskMethod) : _isTaskMethod(request);
164
+ if (touchesTasks) {
165
+ const ctype = String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
166
+ const origin = req.headers.origin;
167
+ const reject = (status, message) => {
168
+ res.writeHead(status, { 'Content-Type': 'application/json' });
169
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32600, message: message }, id: null }));
170
+ };
171
+ if (ctype !== 'application/json') { reject(415, 'Task requests require Content-Type: application/json'); return; }
172
+ if (origin !== undefined && !LOOPBACK_ORIGIN_RE.test(String(origin))) { reject(403, 'Task requests require a loopback Origin'); return; }
173
+ }
174
+
154
175
  // Handle batch
155
176
  if (Array.isArray(request)) {
156
- const promises = request.map((r) => Promise.resolve().then(() => this._handler(r)));
177
+ const promises = request.map((r) => Promise.resolve().then(() => this._handler(r, ctx)));
157
178
  Promise.all(promises).then((results) => {
158
179
  const responses = results.filter((r) => r !== null);
159
180
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -171,7 +192,7 @@ class SSETransport {
171
192
  return;
172
193
  }
173
194
 
174
- Promise.resolve().then(() => this._handler(request)).then((response) => {
195
+ Promise.resolve().then(() => this._handler(request, ctx)).then((response) => {
175
196
  res.writeHead(200, { 'Content-Type': 'application/json' });
176
197
  res.end(JSON.stringify(response));
177
198
  }).catch(() => {
@@ -118,7 +118,7 @@ SKIP_DIRS = {"node_modules", ".git", "dist", "build", "__pycache__",
118
118
  ".venv", "venv", "coverage", ".loki"}
119
119
 
120
120
  # Repo-root files that are not under any SOURCE_DIRS entry but do read env.
121
- SOURCE_ROOT_FILES = ("docker-compose.yml", "Dockerfile", "Dockerfile.sandbox",
121
+ SOURCE_ROOT_FILES = ("docker/docker-compose.yml", "docker/Dockerfile", "docker/Dockerfile.sandbox",
122
122
  "package.json", "server.json")
123
123
 
124
124
  # Markdown trees excluded from the scan, each with the reason a reader can
@@ -1,4 +1,4 @@
1
- import{c as L,r as x,j as e,w as re,Z as ne,m as le,h as ie,p as P,F as ye,U as I,X as ce,L as de,x as oe,M as Ae,T as Z,S as we,l as xe,k as me,y as Ce,E as Fe,a0 as Se,a as U,a2 as he}from"./index-B20cSx5G.js";import{B as C}from"./Button-DjVlkh8_.js";import{T as $e}from"./trending-up-DsDNoHZY.js";import{C as z}from"./clock-DTj0kkGT.js";import{A as Be,U as ue,M as De}from"./Avatar-T3nIpoaf.js";import{A as J}from"./activity-Bk-kgqaf.js";import{R as pe}from"./refresh-cw-CzKvrONq.js";import{G as ze}from"./globe-Y6IK2RPW.js";import{C as Le}from"./chevron-left-AYWCXHf8.js";import{C as je}from"./check-BEzjXppd.js";import{D as Y}from"./dollar-sign-XuYFmAOY.js";import{U as Te}from"./user-3Zez1WOK.js";import{D as Me}from"./database-DdfBaowG.js";import{L as Ue}from"./lock-BKPpyt20.js";import{S as Re}from"./scroll-text-CSkiyax8.js";import{S as Ie}from"./shield-alert-CbpHWQQX.js";/**
1
+ import{c as L,r as x,j as e,w as re,Z as ne,m as le,h as ie,p as P,F as ye,U as I,X as ce,L as de,x as oe,M as Ae,T as Z,S as we,l as xe,k as me,y as Ce,E as Fe,a0 as Se,a as U,a2 as he}from"./index-BkbXyH8y.js";import{B as C}from"./Button-rYSyIu7X.js";import{T as $e}from"./trending-up-BhnMcM32.js";import{C as z}from"./clock-BOGqwgt9.js";import{A as Be,U as ue,M as De}from"./Avatar-7GtOQvsO.js";import{A as J}from"./activity-DNQiBDDL.js";import{R as pe}from"./refresh-cw-DsWLXgs_.js";import{G as ze}from"./globe-r-xCP4DE.js";import{C as Le}from"./chevron-left-BJiy55Ht.js";import{C as je}from"./check-CsF-XtQp.js";import{D as Y}from"./dollar-sign-6qWGAkZo.js";import{U as Te}from"./user-Bks3-GYv.js";import{D as Me}from"./database-BJePscl_.js";import{L as Ue}from"./lock-CZZMxP0U.js";import{S as Re}from"./scroll-text-smySzlrX.js";import{S as Ie}from"./shield-alert-BD7FDlh-.js";/**
2
2
  * @license lucide-react v0.577.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
1
- import{c as l,j as s}from"./index-B20cSx5G.js";/**
1
+ import{c as l,j as s}from"./index-BkbXyH8y.js";/**
2
2
  * @license lucide-react v0.577.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -1 +1 @@
1
- import{j as e}from"./index-B20cSx5G.js";import{C as r}from"./clock-DTj0kkGT.js";import{C as a}from"./circle-alert-BNg2uSRq.js";import{C as l}from"./check-BEzjXppd.js";const i={completed:"bg-[#1FC5A8]/10 text-[#1FC5A8]",running:"bg-[#553DE9]/10 text-[#553DE9]",failed:"bg-[#C45B5B]/10 text-[#C45B5B]",started:"bg-[#D4A03C]/10 text-[#D4A03C]",empty:"bg-[#F8F4F0] text-[#6B6960]",version:"bg-[#553DE9]/10 text-[#553DE9]"};function o({status:t}){switch(t){case"completed":return e.jsx(l,{size:12});case"running":return e.jsxs("span",{className:"relative flex h-2 w-2",children:[e.jsx("span",{className:"animate-ping motion-reduce:animate-none absolute inline-flex h-full w-full rounded-full bg-current opacity-75"}),e.jsx("span",{className:"relative inline-flex rounded-full h-2 w-2 bg-current"})]});case"failed":return e.jsx(a,{size:12});case"started":return e.jsx(r,{size:12});default:return null}}function f({status:t,children:n,className:s=""}){return e.jsxs("span",{className:["inline-flex items-center gap-1 rounded-btn px-2.5 py-0.5 text-xs font-semibold",i[t],s].filter(Boolean).join(" "),children:[e.jsx(o,{status:t}),n]})}export{f as B};
1
+ import{j as e}from"./index-BkbXyH8y.js";import{C as r}from"./clock-BOGqwgt9.js";import{C as a}from"./circle-alert-D6-pQQWq.js";import{C as l}from"./check-CsF-XtQp.js";const i={completed:"bg-[#1FC5A8]/10 text-[#1FC5A8]",running:"bg-[#553DE9]/10 text-[#553DE9]",failed:"bg-[#C45B5B]/10 text-[#C45B5B]",started:"bg-[#D4A03C]/10 text-[#D4A03C]",empty:"bg-[#F8F4F0] text-[#6B6960]",version:"bg-[#553DE9]/10 text-[#553DE9]"};function o({status:t}){switch(t){case"completed":return e.jsx(l,{size:12});case"running":return e.jsxs("span",{className:"relative flex h-2 w-2",children:[e.jsx("span",{className:"animate-ping motion-reduce:animate-none absolute inline-flex h-full w-full rounded-full bg-current opacity-75"}),e.jsx("span",{className:"relative inline-flex rounded-full h-2 w-2 bg-current"})]});case"failed":return e.jsx(a,{size:12});case"started":return e.jsx(r,{size:12});default:return null}}function f({status:t,children:n,className:s=""}){return e.jsxs("span",{className:["inline-flex items-center gap-1 rounded-btn px-2.5 py-0.5 text-xs font-semibold",i[t],s].filter(Boolean).join(" "),children:[e.jsx(o,{status:t}),n]})}export{f as B};
@@ -1 +1 @@
1
- import{r as p,j as t}from"./index-B20cSx5G.js";const m={primary:"bg-[#553DE9] text-white hover:bg-[#4432c4] shadow-button rounded-btn",secondary:"border border-[#553DE9] text-[#553DE9] hover:bg-[#E8E4FD] bg-transparent rounded-btn",ghost:"text-[#36342E] hover:bg-[#F8F4F0] rounded-btn",danger:"bg-[#C45B5B]/10 text-[#C45B5B] border border-[#C45B5B]/20 hover:bg-[#C45B5B]/20 rounded-btn"},b={sm:"px-3 py-1.5 text-xs",md:"px-4 py-2 text-sm",lg:"px-6 py-3 text-base"},u={sm:14,md:16,lg:18};function h({size:e}){return t.jsxs("svg",{className:"animate-spin",width:e,height:e,viewBox:"0 0 24 24",fill:"none",children:[t.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),t.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"})]})}const B=p.forwardRef(({variant:e="primary",size:o="md",icon:n,iconRight:i,loading:r=!1,disabled:a,className:c="",children:l,...x},d)=>{const s=u[o];return t.jsxs("button",{ref:d,disabled:a||r,className:["inline-flex items-center justify-center gap-2 font-medium transition-colors",m[e],b[o],(a||r)&&"opacity-60 cursor-not-allowed",c].filter(Boolean).join(" "),...x,children:[r?t.jsx(h,{size:s}):n?t.jsx(n,{size:s}):null,l,i&&!r&&t.jsx(i,{size:s})]})});B.displayName="Button";export{B};
1
+ import{r as p,j as t}from"./index-BkbXyH8y.js";const m={primary:"bg-[#553DE9] text-white hover:bg-[#4432c4] shadow-button rounded-btn",secondary:"border border-[#553DE9] text-[#553DE9] hover:bg-[#E8E4FD] bg-transparent rounded-btn",ghost:"text-[#36342E] hover:bg-[#F8F4F0] rounded-btn",danger:"bg-[#C45B5B]/10 text-[#C45B5B] border border-[#C45B5B]/20 hover:bg-[#C45B5B]/20 rounded-btn"},b={sm:"px-3 py-1.5 text-xs",md:"px-4 py-2 text-sm",lg:"px-6 py-3 text-base"},u={sm:14,md:16,lg:18};function h({size:e}){return t.jsxs("svg",{className:"animate-spin",width:e,height:e,viewBox:"0 0 24 24",fill:"none",children:[t.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),t.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"})]})}const B=p.forwardRef(({variant:e="primary",size:o="md",icon:n,iconRight:i,loading:r=!1,disabled:a,className:c="",children:l,...x},d)=>{const s=u[o];return t.jsxs("button",{ref:d,disabled:a||r,className:["inline-flex items-center justify-center gap-2 font-medium transition-colors",m[e],b[o],(a||r)&&"opacity-60 cursor-not-allowed",c].filter(Boolean).join(" "),...x,children:[r?t.jsx(h,{size:s}):n?t.jsx(n,{size:s}):null,l,i&&!r&&t.jsx(i,{size:s})]})});B.displayName="Button";export{B};
@@ -1,4 +1,4 @@
1
- import{c as $,r as l,j as e,G as we,Q as Se,F as Ce,h as Re,T as U,Z as Ee,a as S,V as Me,S as $e,Y as Te,X as Pe,P as ie,i as Ae,g as ze,C as Ie,l as Le,N as I,J as _e,K as De,O as Fe,u as Oe}from"./index-B20cSx5G.js";import{P as K,S as He,E as Be}from"./ErrorBoundary-CLjpGLjC.js";import{C as qe,F as Ge,M as Ue,H as Ke,a as We,G as Ve,T as Qe}from"./thumbs-up-DgGpZe-Q.js";import{C as oe}from"./check-BEzjXppd.js";import{C as Ye}from"./clock-DTj0kkGT.js";import{D as Xe}from"./dollar-sign-XuYFmAOY.js";import{U as Ze}from"./upload-gkThrc_I.js";import{L as V}from"./loader-circle-DyGbrEgd.js";/**
1
+ import{c as $,r as l,j as e,G as we,Q as Se,F as Ce,h as Re,T as U,Z as Ee,a as S,V as Me,S as $e,Y as Te,X as Pe,P as ie,i as Ae,g as ze,C as Ie,l as Le,N as I,J as _e,K as De,O as Fe,u as Oe}from"./index-BkbXyH8y.js";import{P as K,S as He,E as Be}from"./ErrorBoundary-CwtvY4xs.js";import{C as qe,F as Ge,M as Ue,H as Ke,a as We,G as Ve,T as Qe}from"./thumbs-up-CAsFzbyE.js";import{C as oe}from"./check-CsF-XtQp.js";import{C as Ye}from"./clock-BOGqwgt9.js";import{D as Xe}from"./dollar-sign-6qWGAkZo.js";import{U as Ze}from"./upload-JVHfdwoA.js";import{L as V}from"./loader-circle-3gosTei9.js";/**
2
2
  * @license lucide-react v0.577.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.