onesensei 0.2.0__tar.gz

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. onesensei-0.2.0/.claude/settings.json +5 -0
  2. onesensei-0.2.0/.github/workflows/release.yml +60 -0
  3. onesensei-0.2.0/.gitignore +15 -0
  4. onesensei-0.2.0/.graphifyignore +7 -0
  5. onesensei-0.2.0/.superpowers/sdd/.gitignore +1 -0
  6. onesensei-0.2.0/.superpowers/sdd/final-review-fix-report.md +106 -0
  7. onesensei-0.2.0/.superpowers/sdd/progress-org-admin-dashboard-poc-DONE.md +25 -0
  8. onesensei-0.2.0/.superpowers/sdd/progress-sensei-pro-licensing-DONE.md +22 -0
  9. onesensei-0.2.0/.superpowers/sdd/progress-work-item-cost-tracking-DONE.md +20 -0
  10. onesensei-0.2.0/.superpowers/sdd/progress.md +21 -0
  11. onesensei-0.2.0/.superpowers/sdd/review-013ee21..8157611.diff +118 -0
  12. onesensei-0.2.0/.superpowers/sdd/review-170bf5c..01d3cd7.diff +228 -0
  13. onesensei-0.2.0/.superpowers/sdd/review-183607c..183607c.diff +7 -0
  14. onesensei-0.2.0/.superpowers/sdd/review-1887fc0..28c543a.diff +158 -0
  15. onesensei-0.2.0/.superpowers/sdd/review-24d08b3..fa4f0c4.diff +212 -0
  16. onesensei-0.2.0/.superpowers/sdd/review-282df05..dec136b.diff +263 -0
  17. onesensei-0.2.0/.superpowers/sdd/review-28c543a..60d140c.diff +100 -0
  18. onesensei-0.2.0/.superpowers/sdd/review-3250b4d..65308ec.diff +638 -0
  19. onesensei-0.2.0/.superpowers/sdd/review-3250b4d..cbec820.diff +87 -0
  20. onesensei-0.2.0/.superpowers/sdd/review-45c9c18..6c9cdf4.diff +140 -0
  21. onesensei-0.2.0/.superpowers/sdd/review-4812860..HEAD.diff +210 -0
  22. onesensei-0.2.0/.superpowers/sdd/review-4f34233..HEAD.diff +110 -0
  23. onesensei-0.2.0/.superpowers/sdd/review-5c7e007..e8275b1.diff +199 -0
  24. onesensei-0.2.0/.superpowers/sdd/review-5ecc523..282df05.diff +190 -0
  25. onesensei-0.2.0/.superpowers/sdd/review-5ecc523..c2a821c.diff +600 -0
  26. onesensei-0.2.0/.superpowers/sdd/review-60d140c..013ee21.diff +391 -0
  27. onesensei-0.2.0/.superpowers/sdd/review-62e4c91..0048df8.diff +123 -0
  28. onesensei-0.2.0/.superpowers/sdd/review-62e4c91..183607c.diff +361 -0
  29. onesensei-0.2.0/.superpowers/sdd/review-62e4c91..3250b4d.diff +79 -0
  30. onesensei-0.2.0/.superpowers/sdd/review-6f9d6f1..45c9c18.diff +155 -0
  31. onesensei-0.2.0/.superpowers/sdd/review-6f9d6f1..6c9cdf4.diff +245 -0
  32. onesensei-0.2.0/.superpowers/sdd/review-713bbe2..7b2bd75.diff +194 -0
  33. onesensei-0.2.0/.superpowers/sdd/review-78bd84a..1887fc0.diff +150 -0
  34. onesensei-0.2.0/.superpowers/sdd/review-7b2bd75..c2a821c.diff +164 -0
  35. onesensei-0.2.0/.superpowers/sdd/review-8157611..06d13d6.diff +296 -0
  36. onesensei-0.2.0/.superpowers/sdd/review-898ccb7..6f9d6f1.diff +649 -0
  37. onesensei-0.2.0/.superpowers/sdd/review-898ccb7..c74153c.diff +254 -0
  38. onesensei-0.2.0/.superpowers/sdd/review-90311d1..ba4c57a.diff +859 -0
  39. onesensei-0.2.0/.superpowers/sdd/review-911b46d..5ecc523.diff +92 -0
  40. onesensei-0.2.0/.superpowers/sdd/review-97355a1..6f9d6f1.diff +154 -0
  41. onesensei-0.2.0/.superpowers/sdd/review-99b5d80..8157611.diff +2072 -0
  42. onesensei-0.2.0/.superpowers/sdd/review-99b5d80..90311d1.diff +234 -0
  43. onesensei-0.2.0/.superpowers/sdd/review-aa41f1e..24d08b3.diff +53 -0
  44. onesensei-0.2.0/.superpowers/sdd/review-ac0ee24..08a676b.diff +435 -0
  45. onesensei-0.2.0/.superpowers/sdd/review-ac0ee24..f452411.diff +66 -0
  46. onesensei-0.2.0/.superpowers/sdd/review-ba4c57a..78bd84a.diff +390 -0
  47. onesensei-0.2.0/.superpowers/sdd/review-c2a821c..898ccb7.diff +122 -0
  48. onesensei-0.2.0/.superpowers/sdd/review-c74153c..5c7e007.diff +119 -0
  49. onesensei-0.2.0/.superpowers/sdd/review-cbec820..183607c.diff +216 -0
  50. onesensei-0.2.0/.superpowers/sdd/review-d2d6546..01d3cd7.diff +607 -0
  51. onesensei-0.2.0/.superpowers/sdd/review-d2d6546..399182c.diff +376 -0
  52. onesensei-0.2.0/.superpowers/sdd/review-d2d6546..ac0ee24.diff +281 -0
  53. onesensei-0.2.0/.superpowers/sdd/review-dec136b..713bbe2.diff +51 -0
  54. onesensei-0.2.0/.superpowers/sdd/review-f452411..170bf5c.diff +182 -0
  55. onesensei-0.2.0/.superpowers/sdd/review-f452411..8b3b7b2.diff +707 -0
  56. onesensei-0.2.0/.superpowers/sdd/review-fa4f0c4..cfa560a.diff +178 -0
  57. onesensei-0.2.0/.superpowers/sdd/review-final.diff +545 -0
  58. onesensei-0.2.0/.superpowers/sdd/review-task2-only.diff +126 -0
  59. onesensei-0.2.0/.superpowers/sdd/review-task3.diff +56 -0
  60. onesensei-0.2.0/.superpowers/sdd/task-1-brief.md +129 -0
  61. onesensei-0.2.0/.superpowers/sdd/task-1-fix-report.md +138 -0
  62. onesensei-0.2.0/.superpowers/sdd/task-1-report.md +109 -0
  63. onesensei-0.2.0/.superpowers/sdd/task-2-brief.md +126 -0
  64. onesensei-0.2.0/.superpowers/sdd/task-2-fix-report.md +106 -0
  65. onesensei-0.2.0/.superpowers/sdd/task-2-report.md +52 -0
  66. onesensei-0.2.0/.superpowers/sdd/task-3-brief.md +211 -0
  67. onesensei-0.2.0/.superpowers/sdd/task-3-fix-report.md +119 -0
  68. onesensei-0.2.0/.superpowers/sdd/task-3-report.md +121 -0
  69. onesensei-0.2.0/.superpowers/sdd/task-4-brief.md +158 -0
  70. onesensei-0.2.0/.superpowers/sdd/task-4-fix-report.md +146 -0
  71. onesensei-0.2.0/.superpowers/sdd/task-4-report.md +97 -0
  72. onesensei-0.2.0/.superpowers/sdd/task-5-brief.md +283 -0
  73. onesensei-0.2.0/.superpowers/sdd/task-5-report.md +105 -0
  74. onesensei-0.2.0/.superpowers/sdd/task-6-brief.md +88 -0
  75. onesensei-0.2.0/.superpowers/sdd/task-6-dojo-report.md +117 -0
  76. onesensei-0.2.0/.superpowers/sdd/task-6-report.md +62 -0
  77. onesensei-0.2.0/.superpowers/sdd/task-7-report.md +187 -0
  78. onesensei-0.2.0/CHANGELOG.md +39 -0
  79. onesensei-0.2.0/CLAUDE.md +59 -0
  80. onesensei-0.2.0/PKG-INFO +516 -0
  81. onesensei-0.2.0/README.md +501 -0
  82. onesensei-0.2.0/docs/ROADMAP_SPEC.md +272 -0
  83. onesensei-0.2.0/docs/sensei v2.md +383 -0
  84. onesensei-0.2.0/docs/superpowers/plans/2026-06-27-backlog-condicional.md +924 -0
  85. onesensei-0.2.0/docs/superpowers/plans/2026-06-28-ai-maturity-insights.md +1042 -0
  86. onesensei-0.2.0/docs/superpowers/plans/2026-06-28-developer-memory.md +1484 -0
  87. onesensei-0.2.0/docs/superpowers/plans/2026-06-29-briefing-into-resumen.md +287 -0
  88. onesensei-0.2.0/docs/superpowers/plans/2026-06-29-coach-insights-frontend.md +369 -0
  89. onesensei-0.2.0/docs/superpowers/plans/2026-06-29-llm-perfil-tab.md +610 -0
  90. onesensei-0.2.0/docs/superpowers/plans/2026-07-05-dojo-global-admin-cross-tenant.md +1022 -0
  91. onesensei-0.2.0/docs/superpowers/plans/2026-07-05-dojo-license-service.md +1124 -0
  92. onesensei-0.2.0/docs/superpowers/plans/2026-07-05-org-admin-dashboard-poc.md +774 -0
  93. onesensei-0.2.0/docs/superpowers/plans/2026-07-05-pro-lock-ui.md +275 -0
  94. onesensei-0.2.0/docs/superpowers/plans/2026-07-05-sensei-pro-licensing-client.md +1436 -0
  95. onesensei-0.2.0/docs/superpowers/plans/2026-07-05-work-item-cost-tracking.md +577 -0
  96. onesensei-0.2.0/docs/superpowers/specs/2026-06-27-backlog-condicional-design.md +85 -0
  97. onesensei-0.2.0/docs/superpowers/specs/2026-06-28-ai-maturity-insights.md +142 -0
  98. onesensei-0.2.0/docs/superpowers/specs/2026-06-28-developer-memory-design.md +277 -0
  99. onesensei-0.2.0/docs/superpowers/specs/2026-06-29-briefing-into-resumen.md +134 -0
  100. onesensei-0.2.0/docs/superpowers/specs/2026-06-29-coach-insights-frontend.md +160 -0
  101. onesensei-0.2.0/docs/superpowers/specs/2026-06-29-llm-perfil-tab.md +167 -0
  102. onesensei-0.2.0/docs/superpowers/specs/2026-07-05-dojo-global-admin-cross-tenant.md +117 -0
  103. onesensei-0.2.0/docs/superpowers/specs/2026-07-05-sensei-pro-tiering-design.md +162 -0
  104. onesensei-0.2.0/dojo/.dockerignore +9 -0
  105. onesensei-0.2.0/dojo/.env.example +1 -0
  106. onesensei-0.2.0/dojo/Dockerfile +30 -0
  107. onesensei-0.2.0/dojo/README.md +78 -0
  108. onesensei-0.2.0/dojo/dojo/__init__.py +0 -0
  109. onesensei-0.2.0/dojo/dojo/admin_cli.py +134 -0
  110. onesensei-0.2.0/dojo/dojo/app.py +60 -0
  111. onesensei-0.2.0/dojo/dojo/db.py +92 -0
  112. onesensei-0.2.0/dojo/dojo/security.py +85 -0
  113. onesensei-0.2.0/dojo/pyproject.toml +27 -0
  114. onesensei-0.2.0/dojo/tests/__init__.py +0 -0
  115. onesensei-0.2.0/dojo/tests/conftest.py +6 -0
  116. onesensei-0.2.0/dojo/tests/test_admin_cli.py +88 -0
  117. onesensei-0.2.0/dojo/tests/test_app.py +77 -0
  118. onesensei-0.2.0/dojo/tests/test_db.py +72 -0
  119. onesensei-0.2.0/dojo/tests/test_security.py +70 -0
  120. onesensei-0.2.0/install.sh +36 -0
  121. onesensei-0.2.0/pyproject.toml +45 -0
  122. onesensei-0.2.0/scripts/graph_context.py +93 -0
  123. onesensei-0.2.0/sensei/__init__.py +2 -0
  124. onesensei-0.2.0/sensei/_resources.py +13 -0
  125. onesensei-0.2.0/sensei/adapters.py +169 -0
  126. onesensei-0.2.0/sensei/baseline.json +5 -0
  127. onesensei-0.2.0/sensei/cli.py +313 -0
  128. onesensei-0.2.0/sensei/coach.py +447 -0
  129. onesensei-0.2.0/sensei/collectors/__init__.py +0 -0
  130. onesensei-0.2.0/sensei/collectors/claude_code.py +437 -0
  131. onesensei-0.2.0/sensei/collectors/copilot.py +278 -0
  132. onesensei-0.2.0/sensei/collectors/git_status.py +93 -0
  133. onesensei-0.2.0/sensei/collectors/kiro.py +429 -0
  134. onesensei-0.2.0/sensei/collectors/opencode.py +501 -0
  135. onesensei-0.2.0/sensei/config.default.json +98 -0
  136. onesensei-0.2.0/sensei/config.py +120 -0
  137. onesensei-0.2.0/sensei/content_parser.py +493 -0
  138. onesensei-0.2.0/sensei/costo.py +251 -0
  139. onesensei-0.2.0/sensei/engine.py +784 -0
  140. onesensei-0.2.0/sensei/graph.py +127 -0
  141. onesensei-0.2.0/sensei/licensing.py +219 -0
  142. onesensei-0.2.0/sensei/live.py +280 -0
  143. onesensei-0.2.0/sensei/llm_coach.py +980 -0
  144. onesensei-0.2.0/sensei/maturity.py +303 -0
  145. onesensei-0.2.0/sensei/morning.py +296 -0
  146. onesensei-0.2.0/sensei/profiler.py +173 -0
  147. onesensei-0.2.0/sensei/roi.py +228 -0
  148. onesensei-0.2.0/sensei/server.py +783 -0
  149. onesensei-0.2.0/sensei/setup_wizard.py +143 -0
  150. onesensei-0.2.0/sensei/ui/index.html +3869 -0
  151. onesensei-0.2.0/sensei/ui/live.html +121 -0
  152. onesensei-0.2.0/sensei/ui/settings.html +1029 -0
  153. onesensei-0.2.0/sensei/ui/setup.html +187 -0
  154. onesensei-0.2.0/sensei/voice.py +55 -0
  155. onesensei-0.2.0/sensei.spec +70 -0
  156. onesensei-0.2.0/tests/__init__.py +0 -0
  157. onesensei-0.2.0/tests/fixtures.py +61 -0
  158. onesensei-0.2.0/tests/integration/__init__.py +0 -0
  159. onesensei-0.2.0/tests/integration/test_sensei_dojo_interop.py +47 -0
  160. onesensei-0.2.0/tests/smoke.py +597 -0
  161. onesensei-0.2.0/tests/test_adapters.py +78 -0
  162. onesensei-0.2.0/tests/test_burn.py +55 -0
  163. onesensei-0.2.0/tests/test_cli_ascii.py +34 -0
  164. onesensei-0.2.0/tests/test_cli_costo.py +135 -0
  165. onesensei-0.2.0/tests/test_cli_dojo.py +42 -0
  166. onesensei-0.2.0/tests/test_cli_insights.py +59 -0
  167. onesensei-0.2.0/tests/test_cli_profile_licensing_gate.py +19 -0
  168. onesensei-0.2.0/tests/test_client_budgets.py +57 -0
  169. onesensei-0.2.0/tests/test_coach_backlog.py +35 -0
  170. onesensei-0.2.0/tests/test_content_parser.py +88 -0
  171. onesensei-0.2.0/tests/test_costo.py +367 -0
  172. onesensei-0.2.0/tests/test_engine_backlog.py +44 -0
  173. onesensei-0.2.0/tests/test_engine_licensing_gate.py +54 -0
  174. onesensei-0.2.0/tests/test_graph.py +78 -0
  175. onesensei-0.2.0/tests/test_licensing_core.py +171 -0
  176. onesensei-0.2.0/tests/test_licensing_network.py +261 -0
  177. onesensei-0.2.0/tests/test_live.py +113 -0
  178. onesensei-0.2.0/tests/test_llm_coach.py +70 -0
  179. onesensei-0.2.0/tests/test_maturity.py +381 -0
  180. onesensei-0.2.0/tests/test_opencode_parity.py +115 -0
  181. onesensei-0.2.0/tests/test_profile_cmd.py +55 -0
  182. onesensei-0.2.0/tests/test_profiler.py +72 -0
  183. onesensei-0.2.0/tests/test_repeated_context.py +65 -0
  184. onesensei-0.2.0/tests/test_roi_backlog.py +25 -0
  185. onesensei-0.2.0/tests/test_roi_total_value.py +51 -0
  186. onesensei-0.2.0/tests/test_server_apply.py +98 -0
  187. onesensei-0.2.0/tests/test_server_export.py +70 -0
  188. onesensei-0.2.0/tests/test_server_history.py +95 -0
  189. onesensei-0.2.0/tests/test_server_licensing_gate.py +101 -0
  190. onesensei-0.2.0/tests/test_session_autopsy.py +58 -0
  191. onesensei-0.2.0/tests/test_setup_wizard.py +106 -0
  192. onesensei-0.2.0/tests/test_smoke_ui.py +187 -0
  193. onesensei-0.2.0/tests/test_whatif.py +85 -0
  194. onesensei-0.2.0/uv.lock +987 -0
@@ -0,0 +1,5 @@
1
+ {
2
+ "enabledPlugins": {
3
+ "frontend-design@claude-plugins-official": true
4
+ }
5
+ }
@@ -0,0 +1,60 @@
1
+ name: Build & Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build-macos:
10
+ name: macOS (arm64 + x86_64)
11
+ runs-on: macos-14 # Apple Silicon; compila universal si se quiere
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+
19
+ - name: Instalar dependencias de build
20
+ run: pip install pyinstaller hatchling
21
+
22
+ - name: Build binario
23
+ run: pyinstaller sensei.spec
24
+
25
+ - name: Renombrar con plataforma
26
+ run: mv dist/sensei dist/sensei-macos-arm64
27
+
28
+ - name: Crear release y subir asset
29
+ uses: softprops/action-gh-release@v2
30
+ with:
31
+ files: dist/sensei-macos-arm64
32
+ generate_release_notes: true
33
+ env:
34
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35
+
36
+ build-macos-x86:
37
+ name: macOS (Intel x86_64)
38
+ runs-on: macos-13
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+
42
+ - uses: actions/setup-python@v5
43
+ with:
44
+ python-version: "3.12"
45
+
46
+ - name: Instalar dependencias de build
47
+ run: pip install pyinstaller hatchling
48
+
49
+ - name: Build binario
50
+ run: pyinstaller sensei.spec
51
+
52
+ - name: Renombrar con plataforma
53
+ run: mv dist/sensei dist/sensei-macos-x86_64
54
+
55
+ - name: Subir a release existente
56
+ uses: softprops/action-gh-release@v2
57
+ with:
58
+ files: dist/sensei-macos-x86_64
59
+ env:
60
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ .venv/
7
+ .pytest_cache/
8
+ .atlas/
9
+ .claude/settings.local.json
10
+ .superpowers/brainstorm/
11
+ graphify-out/
12
+ dojo/dojo.db
13
+ dojo/*.db
14
+ dojo/.env
15
+ .worktrees/
@@ -0,0 +1,7 @@
1
+ .superpowers/
2
+ .claude/
3
+ docs/superpowers/
4
+ graphify-out/
5
+ .env
6
+ .env.*
7
+ .venv/
@@ -0,0 +1 @@
1
+ *
@@ -0,0 +1,106 @@
1
+ # Final review fix report — cmd_costo (by_work_item honesty + --export/--merge conflict)
2
+
3
+ ## Contexto
4
+
5
+ Dos hallazgos "Important" de la revisión final de todo el branch sobre código ya mergeado
6
+ (`sensei/cli.py`, `cmd_costo`, HEAD `c2a821c` en `sensei/v2`).
7
+
8
+ ## Fix 1 — `by_work_item` es un no-op silencioso → ahora es honesto
9
+
10
+ `by_work_item` se aceptaba y pasaba desde `main()` pero nunca se leía en el cuerpo de
11
+ `cmd_costo`: la tabla por work item se mostraba siempre, con o sin `--by-work-item`.
12
+ No se cambió el comportamiento (agregar gating real ahora sería prematuro — YAGNI, no
13
+ existe una segunda vista todavía) ni la firma de la función (otros callers/tests la
14
+ invocan con `by_work_item=True/False` explícito).
15
+
16
+ Cambios:
17
+ - Comentario agregado en `sensei/cli.py` justo antes de `rows = costo.by_work_item(payload)`
18
+ explicando por qué el parámetro no gatea nada hoy y cuándo empezará a importar.
19
+ - `help=` de `--by-work-item` en `main()` reescrito para no insinuar que el flag es
20
+ necesario u que cambia el comportamiento: ahora dice explícitamente que agrupar por
21
+ work item es el comportamiento por defecto hoy y que el flag existe por compatibilidad
22
+ con el roadmap (`docs/ROADMAP_SPEC.md`).
23
+ - Sin tests nuevos para este fix (es documentación/honestidad, no hay comportamiento
24
+ nuevo que assertar), según lo pedido en el brief.
25
+
26
+ ## Fix 2 — `--export` + `--merge` juntos: `--merge` ganaba en silencio
27
+
28
+ Antes, `cmd_costo` chequeaba `if merge_pattern:` primero y retornaba sin llegar nunca al
29
+ bloque de `export_dir` — si un usuario pasaba ambos flags, perdía su `--export` sin
30
+ ningún feedback.
31
+
32
+ Cambio: guard agregado al principio del cuerpo de `cmd_costo`, antes del `if merge_pattern:`
33
+ existente:
34
+
35
+ ```python
36
+ if merge_pattern and export_dir:
37
+ print("[sensei] --export y --merge son modos distintos — no se pueden combinar. "
38
+ "Usá --export para generar tu propio archivo, o --merge para sumar exports ya generados.")
39
+ return 2
40
+ ```
41
+
42
+ Código de retorno elegido: `2`. Los otros paths de error en `cmd_costo` usan `return 1`
43
+ (glob sin matches); `2` es un código distinto que no colisiona con ningún otro path
44
+ existente en la función.
45
+
46
+ Test nuevo en `tests/test_cli_costo.py`:
47
+ `test_cmd_costo_export_and_merge_together_is_rejected` — llama a `cmd_costo` con ambos
48
+ `export_dir` y `merge_pattern` truthy, y asegura:
49
+ - `result != 0`
50
+ - el mensaje de error menciona tanto `--export` como `--merge`
51
+ - la carpeta de export (`export_dir`) nunca se creó — prueba de que el código nunca
52
+ llegó al bloque de `costo.export_by_work_item(...)` (no hace falta mockear el glob:
53
+ si el código hubiera seguido al branch de merge, tampoco habría fallado por archivos
54
+ inexistentes porque ese path imprime un mensaje distinto "sin archivos que coincidan",
55
+ pero la aserción decisiva es que `export_dir` nunca existe — si el código hubiera
56
+ llegado al branch de export, la carpeta se habría creado).
57
+
58
+ ## Resultado de tests
59
+
60
+ ```
61
+ uv run pytest tests/test_costo.py tests/test_cli_costo.py -v
62
+ → 26 passed
63
+
64
+ uv run pytest -v --ignore=tests/integration/test_sensei_dojo_interop.py
65
+ → 257 passed
66
+
67
+ uv run python -m unittest tests.smoke -v
68
+ → Ran 42 tests: FAILED (failures=3, errors=2)
69
+ ```
70
+
71
+ `tests/integration/test_sensei_dojo_interop.py` falla en collection
72
+ (`ImportError: cannot import name 'app' from 'dojo'`) — variante local del
73
+ `ModuleNotFoundError: fastapi` mencionado en el brief; confirmado pre-existente
74
+ (reproducido idéntico con `git stash` sobre HEAD antes de mis cambios).
75
+
76
+ `tests/smoke.py`: los mismos 3 failures + 2 errors (`TestOpencodeCollector` x2,
77
+ `TestCoach.test_cache_rule_fires_on_low_hit`, `TestOpencodeCollector.test_engine_combines_sources`,
78
+ `TestServerReadOnly.test_endpoints_and_readonly`) están presentes de forma idéntica
79
+ en HEAD `c2a821c` sin mis cambios (verificado con `git stash` / `stash pop`). No están
80
+ relacionados con `cmd_costo`/`costo.py` y no son introducidos por este fix.
81
+
82
+ ## Archivos modificados
83
+
84
+ - `sensei/cli.py` — guard de `--export`+`--merge`, comentario de honestidad sobre
85
+ `by_work_item`, help text actualizado.
86
+ - `tests/test_cli_costo.py` — un test nuevo.
87
+
88
+ Nota: el working tree tenía además cambios sin commitear en `.gitignore`,
89
+ `docs/ROADMAP_SPEC.md` y varios `docs/superpowers/plans/*.md` no relacionados con esta
90
+ tarea (pre-existentes, de otro trabajo en curso). No los toqué ni los incluí en el commit.
91
+
92
+ ## Self-review
93
+
94
+ - Verifiqué que `cmd_costo` no cambió de firma y que ningún caller (`main()`, tests)
95
+ necesitó ajustes más allá del `help=`.
96
+ - Confirmé que el guard nuevo se ejecuta antes de cualquier I/O (glob, `build_payload`,
97
+ export) — ambos modos quedan bloqueados atómicamente antes de tocar disco.
98
+ - Confirmé con `git stash`/`git diff` que las fallas de `tests/smoke.py` y la de
99
+ collection de integración son pre-existentes y no introducidas por este cambio.
100
+
101
+ ## Concerns
102
+
103
+ - Ninguno bloqueante. El código de retorno `2` es una convención nueva dentro de
104
+ `cmd_costo` (antes solo usaba `0`/`1`); si en el futuro se agregan más modos de error
105
+ mutuamente excluyentes convendría documentar la tabla de exit codes de `costo`, pero
106
+ no era parte del scope de este fix.
@@ -0,0 +1,25 @@
1
+ # SDD Progress Ledger — Org Admin Dashboard (S4-5, PoC)
2
+
3
+ Plan: docs/superpowers/plans/2026-07-05-org-admin-dashboard-poc.md
4
+ Branch: sensei/v2 (main tree, no worktree)
5
+ Branch start: 898ccb7
6
+
7
+ Previous plan's ledger (Work-Item Cost Tracking, complete) archived at:
8
+ .superpowers/sdd/progress-work-item-cost-tracking-DONE.md
9
+
10
+ ## Tasks
11
+
12
+ - [x] Task 1: Basic-tier raw metrics — cache_hit_pct, repeated_context_pct, roi.py total_value
13
+ - [x] Task 2: dev_summary() — combine into one factual, Pro-aware row
14
+
15
+ Task 1: complete (commits 898ccb7..c74153c, review Approved — cache_hit_pct verified with zero coupling to Pro-gated coach.py (grep confirmed), total_value verified by independent numerical re-derivation to sum the full session list not just the top-30 scatter. Implementer caught and fixed a flawed test-data example in the brief's own Step 9 (uniform gradient never produces a strict scatter subset), verified as a genuine fix, not scope creep. Accepted, not fixed: plan-mandated duplication of path-resolution helper vs server.py (Important but by design, noted for future consolidation); tautological "does not import coach" test assertion inherited from brief (Minor, actual verification was via grep in review).)
16
+ - [x] Task 3: Carry dev_summary through export, read it back with by_dev()
17
+
18
+ Task 2: complete (commits c74153c..5c7e007, review Approved — no findings. Four separate keys confirmed, roi_value explicit None (never 0/KeyError) verified including the missing-"pro"-key case, cost correctly summed across all sources, kill-list respected (docstring explicitly cites it).)
19
+
20
+ Task 3: complete (commits 5c7e007..e8275b1, +97355a1 doc fix. Review Approved — sort key literal `key=lambda r: r["dev"]` pinned by source-inspection test, backward-compat 2-arg path verified to omit (not null) dev/dev_summary via real file round-trip, by_dev() tolerates both corrupt JSON and old-format-missing-summary with distinct warnings, aggregate-only reconstruction confirmed. One Important gap found — module docstring not updated to mention dev/dev_summary fields — fixed directly post-review (97355a1), 37/37 tests still pass.)
21
+ - [x] Task 4: CLI wiring — print the per-dev table and "yours" summary line
22
+
23
+ Task 4: complete (commit 6f9d6f1, review Approved — no findings. Control flow matches brief exactly, roi_value uses `is not None` (not truthiness) so a real 0.0 ROI would render correctly, all four dev metrics stay in separate columns/fields (kill-list respected), backward-compat 2-arg/3-arg export_by_work_item call sites both still work, no duplicate costo import, all None-guards on format calls verified safe. 10/10 new+existing test_cli_costo.py, 280/280 full suite, tests.smoke at known baseline.)
24
+
25
+ ## ALL TASKS COMPLETE — proceeding to final whole-branch review (898ccb7..HEAD)
@@ -0,0 +1,22 @@
1
+ # SDD Progress Ledger — Sensei Pro Licensing Client
2
+
3
+ Plan: docs/superpowers/plans/2026-07-05-sensei-pro-licensing-client.md
4
+ Branch: sensei/v2 (main tree, no worktree — companion Dojo plan runs isolated in .worktrees/dojo-license-service)
5
+ Branch start: 99b5d80
6
+
7
+ ## Tasks (plan's original 6, all complete — see below for post-final-review fix)
8
+
9
+ - [x] Task 7 (extra, not in original plan): Gate /api/llm/* — Critical gap found by final whole-branch review (opus). commits 8157611..06d13d6, review Approved. Every endpoint in the required mapping gated with correct PRO_FEATURES name (verified against llm_coach.py function bodies, not just trusted); status/test correctly stay ungated, proven by test. Gate sits before cache-invalidation/payload build. Shared _send_locked() helper de-dupes all 4 403 sites in server.py + a shared constant de-dupes the 2 CLI lock messages. One Important-but-non-functional plan-mandated deviation: _send_locked ended up as (message) not (feature, message) as literally specified — cosmetic API-shape only, behavior/security unaffected, accepted.
10
+
11
+ - [x] Task 1: Core licensing module — storage, signing, verification (Ed25519 fix commit ba4c57a, review Approved. Residual self-forge-with-own-keypair gap accepted as same risk tier as editing source (spec Decision 3) — user chose to mitigate the narrower "forged/invalid token checked against a reachable real Dojo" case instead. Plan's Task 2 updated: refresh() now distinguishes unreachable Dojo (keep cached) from explicit 403/404 rejection (invalidate immediately).)
12
+ - [x] Task 2: Network layer — connect, refresh, disconnect
13
+ - [x] Task 3: CLI wiring — sensei dojo connect/status/disconnect
14
+ - [x] Task 4: Gate Coach, ROI, Developer Memory in engine.build_payload()
15
+ - [x] Task 5: Gate /api/whatif, /api/profile, /api/graph, sensei profile CLI
16
+ - [x] Task 6: Frontend gate for cache reference band
17
+
18
+ Task 6: complete (commits 013ee21..8157611, review Approved — implementer's brief-guessed variable name (`data`) was wrong, real one is `D`; also found and fixed a real bug the brief's literal edit would have shipped: normalize() never forwarded p.pro to the client at all, so the new guard would've stayed permanently false even for paying Pro users. Reviewer independently re-derived the whole chain (licensing.PRO_FEATURES -> entitlements() -> payload["pro"] -> normalize() -> D.pro) and confirmed the fix is necessary, minimal, and leaks nothing beyond 5 booleans. ALL 6 TASKS OF THIS PLAN NOW COMPLETE.)
19
+ Task 5: complete (commits 60d140c..013ee21, review Approved — no fail-open path, all feature names verified against PRO_FEATURES, both self-reported deviations (missing handler.headers in the brief's own test stub; an unlisted affected test_smoke_ui.py fixture) independently verified as correct and properly scoped. Two Important-but-cosmetic plan-mandated findings for final review: 403-locked-response construction duplicated 3x in server.py, the CLI lock message duplicated 2x — both literally specified that way in the brief's step-by-step text, pure DRY cleanup candidate, not a correctness/security issue.)
20
+ Task 4: complete (commits 1887fc0..28c543a..60d140c, review Approved after one fix round — first review found tests/smoke.py wasn't in the regression check scope: one test broke for real (test_roi_in_payload, asserted a "disclaimer" key the locked stub lacks), another went vacuous (test_coach_in_full_payload passed only by coincidence against the stub, contradicting its own "datos reales" docstring). Fix patches licensing.entitlements to all-True around build_payload() in both, verified against PRO_FEATURES completely, and confirmed the other 5 pre-existing smoke.py failures are unrelated (predate this task, checked by running against the parent commit and by reading each failing test's body). One pre-existing (not introduced) Minor noted: test_coach_in_full_payload's assertions don't actually distinguish real Coach output from the locked stub shape, even post-fix.)
21
+ Task 3: complete (commits 78bd84a..1887fc0, review Approved — no Critical/Important. Reviewer independently confirmed the dojo-command interception sits before argparse.ArgumentParser construction by reading the actual file, not just the diff. Full suite 217/217, zero regressions to existing CLI commands.)
22
+ Task 2: complete (commits ba4c57a..78bd84a, review Approved — both security properties independently re-verified by the reviewer (exception ordering read live in file, invalidation tests re-run). One inert Minor: refresh()'s disconnect() call uses the license_path param, not current — safe today since all call sites pass them consistently. Implementer self-caught and fixed a real bug in the brief's own example code: cmd_connect must pass http_post=_http_post_json explicitly, since Python binds default params at def-time and monkeypatching the module attribute later doesn't affect an already-bound default — confirmed correct by reviewer.)
@@ -0,0 +1,20 @@
1
+ # SDD Progress Ledger — Work-Item Cost Tracking (Sprint 3: S3-1/S3-2/S3-5)
2
+
3
+ Plan: docs/superpowers/plans/2026-07-05-work-item-cost-tracking.md
4
+ Branch: sensei/v2 (main tree, no worktree — single plan, no parallel stream right now)
5
+ Branch start: 5ecc523
6
+
7
+ Previous plan's ledger (Sensei Pro Licensing Client, complete, merged) archived at:
8
+ .superpowers/sdd/progress-sensei-pro-licensing-DONE.md
9
+
10
+ ## Tasks
11
+
12
+ - [x] Task 1: Automatic work-item inference (S3-1)
13
+ - [x] Task 2: `sensei costo --by-work-item` (S3-2)
14
+
15
+ Task 1: complete (commits 5ecc523..282df05, review Approved — regex/fallback chain verified live for all documented examples, wiring covers all sources not just one, no manual-tagging path. Deviation from brief's guessed test patch target was necessary (brief guessed a nonexistent function name) and resulted in a stronger, less-mocked integration test using the pre-existing tests/fixtures.py:make_fake_claude helper.)
16
+ - [x] Task 3: Export/merge across devs (S3-5)
17
+
18
+ Task 3: complete (commits 713bbe2..7b2bd75..c2a821c, review Approved after one fix round — three Important findings on the LOCAL ONLY exception function all closed: export now rebuilds rows with only 3 allowed keys by construction (not caller discipline), filename adds hostname to reduce collision risk, merge skips a corrupt file with a named warning instead of crashing the whole team merge. Every fix test asserts the actual guarantee (raw-text leak absence, correct totals with a bad file present), not just non-crash. ALL 3 TASKS OF THIS PLAN NOW COMPLETE.)
19
+ FINAL WHOLE-BRANCH REVIEW (opus): commits c2a821c..898ccb7 — Approved after one fix round. Two Important findings caught only at whole-branch level (no single task review saw them): --by-work-item was a complete no-op (never read in cmd_costo, table showed unconditionally regardless), and --export+--merge together silently discarded --export with no warning. Both fixed: by_work_item's non-gating status documented honestly in code+help text rather than adding premature gating logic (only one view exists today); --export+--merge now rejected with a clear error before either mode runs, proven by a test asserting export_dir is never even created. All principle guarantees (LOCAL ONLY exception airtight, ZERO DEPS intact, kill-list respected, HONEST NUMBERS/sin-asignar consistent end-to-end) reconfirmed at whole-branch level. Remaining Minor findings (no filename-hostname test, corrupt-file granularity is per-file not per-row, no single end-to-end infer->export->merge test) explicitly accepted, not blocking. PLAN FULLY COMPLETE.
20
+ Task 2: complete (commits 282df05..dec136b..713bbe2, review Approved after one small fix — sin asignar bucket never dropped, no argparse collisions with existing commands, cost never recomputed. One Important-plan-mandated gap (no real main()/argparse dispatch test, only direct cmd_costo calls) fixed by adding one test mirroring the dojo dispatch test pattern; verified directly, clean.)
@@ -0,0 +1,21 @@
1
+ # SDD Progress Ledger — Pro Lock UI (candado + CTA en tabs Pro)
2
+
3
+ Plan: docs/superpowers/plans/2026-07-05-pro-lock-ui.md
4
+ Branch: sensei/v2 (main tree, no worktree)
5
+ Branch start: 6f9d6f1
6
+
7
+ Previous plan's ledger (Org Admin Dashboard PoC, complete) archived at:
8
+ .superpowers/sdd/progress-org-admin-dashboard-poc-DONE.md
9
+
10
+ ## Tasks
11
+
12
+ - [x] Task 1: Helper compartido de candado + fix del bug de Coach
13
+ - [x] Task 2: Extender el candado a ROI, Perfil y Whatif
14
+
15
+ Task 1: complete (commit 45c9c18, review Approved — no findings. `_coachReal` fix verified as `p.coach && !p.coach.locked`, `applyProLocks` guards `!D.pro` first, strict `=== false` checks confirmed safe against licensing.entitlements()'s always-boolean contract, demo HTML confirmed genuinely overwritten (not just flag-fixed), tab icon dedup guarded, CTA matches CLI's _PROFILE_LOCKED_MSG verbatim. Bonus fix beyond brief: stale "5" badge count on Coach tab also cleared when locked. 24/24 test_smoke_ui.py, 282/282 full suite, tests.smoke at known baseline.)
16
+
17
+ Task 2: complete (commit 6c9cdf4, review Approved. ROI/.stat-row replacement, Perfil canvas+adapter hiding, Whatif window._senseiData check, and the Perfil tab-switch guard all verified genuine and idempotent, all strictly === false, no tab hidden, shared _proLockHtml reused everywhere. One Important finding, deferred not fixed here: the app has a live 120s polling loop (setInterval fetchAndRender, no page reload) — so a dev who runs `sensei dojo connect` mid-session stays permanently locked on ROI/Perfil/Whatif (and keeps the 🔒 tab icon from Task 1) until a manual reload, because none of the lock branches has a reverse/unlock path. Coach happens to self-heal only as a side effect of renderCoach always rebuilding its panel when real — not a deliberate mechanism. Out of this task's stated scope; flagged for the final whole-branch review and the user's call on a follow-up. 284/284 full suite, tests.smoke at known baseline.)
18
+
19
+ ## ALL TASKS COMPLETE — final whole-branch review: Ready to merge (no Critical, one Important deferred)
20
+
21
+ Final review (6f9d6f1..6c9cdf4, opus): Ready to merge. applyProLocks cohesive across both tasks, single _proLockHtml mechanism, strict === false everywhere, tabs never hidden, CTA verbatim, idempotent under the 120s live poll. One Important finding adjudicated as safe-direction and non-blocking: locked surfaces (ROI/Perfil/Whatif/tab icons) don't self-heal on a live reconnect mid-session without a page reload — never leaks fake data, just stays conservative until F5. Added to docs/ROADMAP_SPEC.md's Backlog condicional as item M (2026-07-05) rather than fixed in-branch, per the reviewer's explicit recommendation (a symmetric-unlock fix inverts the plan's one-way lock design at its most sensitive boundary — riskier rushed than deferred).
@@ -0,0 +1,118 @@
1
+ # Review package: 013ee21..HEAD
2
+
3
+ ## Commits
4
+ 8157611 feat(ui): gate cache reference band behind Pro entitlement
5
+
6
+ ## Files changed
7
+ sensei/ui/index.html | 3 ++-
8
+ tests/test_smoke_ui.py | 22 ++++++++++++++++++++++
9
+ 2 files changed, 24 insertions(+), 1 deletion(-)
10
+
11
+ ## Diff
12
+ diff --git a/sensei/ui/index.html b/sensei/ui/index.html
13
+ index d700457..9a0c354 100644
14
+ --- a/sensei/ui/index.html
15
+ +++ b/sensei/ui/index.html
16
+ @@ -923,20 +923,21 @@ function normalize(p) {
17
+ }
18
+
19
+ // Coach real (insights + sugerencias de skills desde reglas)
20
+ if (p.coach) { d.coach = p.coach; d._coachReal = true; }
21
+ if (p.maturity) d.maturity = p.maturity;
22
+
23
+ // Raw sources para análisis de sesiones en el coach
24
+ if (p.sources) d._sources = p.sources;
25
+ if (p.goals) d.goals = p.goals;
26
+ if (p.features) d.features = p.features;
27
+ + if (p.pro) d.pro = p.pro;
28
+ if (p._history_days) d._historyDays = p._history_days;
29
+ if (p.llm_narrative) d._llmNarrative = p.llm_narrative;
30
+ if (p.user_name) d._userName = p.user_name;
31
+
32
+ // Ya no queda ningún dato demo en modo live: burn se mide via /api/burn
33
+ return d;
34
+ }
35
+
36
+ /* ── 3. RENDER (funciones puras: leen DATA, escriben DOM) ──────── */
37
+ function renderHeader(D) {
38
+ @@ -3351,21 +3352,21 @@ function renderAll(D) {
39
+ </div>
40
+ <button class="llm-explain-btn" style="margin-top:10px"
41
+ onclick="loadLLMAnomaly(this)"
42
+ data-session="${oh.name}" data-project="${oh.project}"
43
+ data-cost="${oh.cost.toFixed(2)}" data-tokens="0" data-kind="rose">¿Por qué?</button>
44
+ <div class="llm-explain-panel"></div>`;
45
+ nowPanel.appendChild(card);
46
+ }
47
+ }
48
+ // Cache reference band (G) — solo si feature habilitado y source no vacío
49
+ - if (feats.cache_reference_band && feats.cache_reference_band.enabled && feats.cache_reference_band.source) {
50
+ + if (feats.cache_reference_band && feats.cache_reference_band.enabled && feats.cache_reference_band.source && D.pro && D.pro.cache_reference_band) {
51
+ const band = feats.cache_reference_band;
52
+ const coachPanel = document.getElementById('coach');
53
+ if (coachPanel && !document.getElementById('cache-band-ref')) {
54
+ const ref = document.createElement('div');
55
+ ref.id = 'cache-band-ref';
56
+ ref.style.cssText = 'font-family:"JetBrains Mono",monospace;font-size:11px;color:var(--text-faint);background:var(--bg-elev);border:1px solid var(--line);border-radius:8px;padding:10px 14px;margin-top:12px;';
57
+ const p50 = band.p50 != null ? `p50 ${band.p50}%` : '';
58
+ const p90 = band.p90 != null ? `· p90 ${band.p90}%` : '';
59
+ ref.innerHTML = `Referencia cache hit — ${p50} ${p90} · Fuente: <em>${band.source}</em>`;
60
+ coachPanel.appendChild(ref);
61
+ diff --git a/tests/test_smoke_ui.py b/tests/test_smoke_ui.py
62
+ index 5dd2c06..50d9237 100644
63
+ --- a/tests/test_smoke_ui.py
64
+ +++ b/tests/test_smoke_ui.py
65
+ @@ -4,20 +4,21 @@ Sin navegador ni dependencias (ZERO DEPS): valida códigos de estado, formas
66
+ mínimas de respuesta y que el dashboard servido contenga los contenedores y
67
+ funciones que los endpoints alimentan — el pegamento que los unit tests no ven.
68
+ """
69
+ from __future__ import annotations
70
+ import json
71
+ import threading
72
+ import time
73
+ import urllib.error
74
+ import urllib.request
75
+ from http.server import ThreadingHTTPServer
76
+ +from pathlib import Path
77
+
78
+ import pytest
79
+
80
+ from sensei import server
81
+
82
+ _PAYLOAD = {
83
+ "totals": {"cost": 10.0, "tokens": 1000, "clients": 1},
84
+ "sources": {"claude_code": {"by_session": [], "by_model": {}}},
85
+ "projects": [], "trend": [], "coach": {"insights": [], "skill_suggestions": [],
86
+ "summary": {"insights": 0, "suggestions": 0}},
87
+ @@ -118,10 +119,31 @@ def test_live_es_pagina_propia(srv):
88
+ html = body.decode("utf-8")
89
+ for needle in ("En vivo", "sensei statusline", "/api/live"):
90
+ assert needle in html, f"falta {needle!r} en live.html"
91
+
92
+
93
+ def test_settings_trae_objetivos(srv):
94
+ _, body = _get(srv, "/settings")
95
+ html = body.decode("utf-8")
96
+ for element_id in ("goal-budget", "goal-score", "client-budgets-wrap"):
97
+ assert f'id="{element_id}"' in html, f"falta #{element_id} en settings.html"
98
+ +
99
+ +
100
+ +def test_cache_reference_band_checks_pro_entitlement():
101
+ + """La banda de referencia de cache es una feature Pro (licensing.PRO_FEATURES).
102
+ +
103
+ + El guard en renderAll(D) no debe depender solo del feature flag (feats,
104
+ + config local) — también debe exigir el entitlement real calculado por
105
+ + engine.build_payload() en payload["pro"]["cache_reference_band"], que
106
+ + llega al front como D.pro (no `feats`, no `data`: la función que envuelve
107
+ + este guard es renderAll(D) y feats = D.features).
108
+ + """
109
+ + html = (Path(__file__).parent.parent / "sensei" / "ui" / "index.html").read_text(encoding="utf-8")
110
+ + guard_line = (
111
+ + "if (feats.cache_reference_band && feats.cache_reference_band.enabled && "
112
+ + "feats.cache_reference_band.source && D.pro && D.pro.cache_reference_band) {"
113
+ + )
114
+ + assert guard_line in html
115
+ + # Sin esto, D.pro nunca existiría (normalize() no reenviaba `pro` desde el
116
+ + # payload del server) y el guard de arriba sería siempre false — la banda
117
+ + # jamás se mostraría, ni para usuarios Pro.
118
+ + assert "if (p.pro) d.pro = p.pro;" in html
@@ -0,0 +1,228 @@
1
+ # Review package: 170bf5c..HEAD
2
+
3
+ ## Commits
4
+ 01d3cd7 fix(ui): remove out-of-scope loadLLMRoiAnalysis from task-3 commit
5
+ a2d9358 fix: cache LLM en disco — persiste entre reinicios del servidor
6
+
7
+ ## Files changed
8
+ sensei/llm_coach.py | 51 +++++++++++++++++++++++++++++++++++++++------------
9
+ sensei/server.py | 20 +++++++++++++++++++-
10
+ sensei/ui/index.html | 23 -----------------------
11
+ 3 files changed, 58 insertions(+), 36 deletions(-)
12
+
13
+ ## Diff
14
+ diff --git a/sensei/llm_coach.py b/sensei/llm_coach.py
15
+ index 27afbb7..a59fbfb 100644
16
+ --- a/sensei/llm_coach.py
17
+ +++ b/sensei/llm_coach.py
18
+ @@ -13,44 +13,41 @@ Features:
19
+ claudemd_for_project() → CLAUDE.md sugerido para un proyecto (Proyectos tab)
20
+ skill_template() → template de skill listo para copiar (Coach skills)
21
+ explain_anomaly() → explicación de sesión rose (ROI tab)
22
+ daily_briefing() → script oral para el orbe del Resumen tab
23
+ """
24
+ from __future__ import annotations
25
+ import shutil
26
+ import subprocess
27
+ import json
28
+ import time
29
+ +from pathlib import Path
30
+
31
+
32
+ TIMEOUT = 30 # segundos por subprocess
33
+ CACHE_TTL = 900 # 15 min — narrativa coach
34
+ TTL_RECAP = 3600 # 1 hora — recap semanal
35
+ TTL_MD = 3600 # 1 hora — CLAUDE.md por proyecto
36
+ TTL_SKILL = 1800 # 30 min — skill templates
37
+ TTL_BRIEFING = 1800 # 30 min — briefing oral del orbe
38
+ TTL_ANOMALY = 1800 # 30 min — explicaciones de sesión
39
+ TTL_PROFILE_NARRATIVE = 3600 # 1 hora — narrativa del grafo
40
+ TTL_CLAUDEMD_GLOBAL = 3600 # 1 hora — CLAUDE.md global
41
+ TTL_NODE_EXPLAIN = 1800 # 30 min — explicación por nodo
42
+
43
+ -_caches: dict[str, dict] = {
44
+ - "narrative": {},
45
+ - "recap": {},
46
+ - "claudemd": {}, # key: project name
47
+ - "skill": {}, # key: skill name
48
+ - "anomaly": {}, # key: session name
49
+ - "briefing": {},
50
+ - "profile_narrative": {}, # key: hash del prompt con grafo
51
+ - "claudemd_global": {}, # key: hash del prompt con grafo + md
52
+ - "node_explain": {}, # key: hash de "name:type"
53
+ -}
54
+ +_caches: dict[str, dict] = {}
55
+ +_CACHE_DIR = Path.home() / ".sensei" / "cache" / "llm"
56
+ +
57
+ +
58
+ +def _cache_file(bucket: str, sub_key: str) -> Path:
59
+ + safe = sub_key.replace("/", "_").replace(":", "_")[:64]
60
+ + return _CACHE_DIR / bucket / f"{safe}.json"
61
+
62
+
63
+ # ── CLI detection ─────────────────────────────────────────────────────────────
64
+
65
+ def _detect_cli() -> tuple[str, list[str]] | None:
66
+ if shutil.which("claude"):
67
+ return ("claude", ["claude", "-p"])
68
+ if shutil.which("opencode"):
69
+ return ("opencode", ["opencode", "run", "--quiet"])
70
+ return None
71
+ @@ -70,33 +67,63 @@ def _run(prompt: str) -> str | None:
72
+ except Exception:
73
+ return None
74
+
75
+
76
+ def _cli_name() -> str:
77
+ cli = _detect_cli()
78
+ return cli[0] if cli else "rule-based"
79
+
80
+
81
+ def _cached(cache_key: str, sub_key: str, ttl: int):
82
+ - """Devuelve el valor cacheado si es fresco, None si expiró."""
83
+ + """Devuelve el valor cacheado (memoria → disco), None si expiró o no existe."""
84
+ store = _caches.get(cache_key, {})
85
+ entry = store.get(sub_key)
86
+ if entry and time.monotonic() < entry.get("expires_at", 0):
87
+ return entry["value"]
88
+ + try:
89
+ + f = _cache_file(cache_key, sub_key)
90
+ + if f.exists():
91
+ + data = json.loads(f.read_text(encoding="utf-8"))
92
+ + if time.time() < data.get("expires_ts", 0):
93
+ + remaining = data["expires_ts"] - time.time()
94
+ + _caches.setdefault(cache_key, {})[sub_key] = {
95
+ + "value": data["value"],
96
+ + "expires_at": time.monotonic() + remaining,
97
+ + }
98
+ + return data["value"]
99
+ + except Exception:
100
+ + pass
101
+ return None
102
+
103
+
104
+ def _store(cache_key: str, sub_key: str, value, ttl: int):
105
+ _caches.setdefault(cache_key, {})[sub_key] = {
106
+ "value": value,
107
+ "expires_at": time.monotonic() + ttl,
108
+ }
109
+ + try:
110
+ + f = _cache_file(cache_key, sub_key)
111
+ + f.parent.mkdir(parents=True, exist_ok=True)
112
+ + f.write_text(
113
+ + json.dumps({"value": value, "expires_ts": time.time() + ttl}, ensure_ascii=False),
114
+ + encoding="utf-8",
115
+ + )
116
+ + except Exception:
117
+ + pass
118
+ +
119
+ +
120
+ +def invalidate_bucket(bucket: str) -> None:
121
+ + """Borra todas las entradas de caché (memoria + disco) de un bucket."""
122
+ + _caches.pop(bucket, None)
123
+ + cache_dir = _CACHE_DIR / bucket
124
+ + if cache_dir.exists():
125
+ + shutil.rmtree(cache_dir, ignore_errors=True)
126
+
127
+
128
+ # ── 1. Narrativa coach ────────────────────────────────────────────────────────
129
+
130
+ def _narrative_prompt(payload: dict) -> str:
131
+ m = payload.get("maturity", {})
132
+ t = payload.get("totals", {})
133
+ projects = payload.get("projects", [])
134
+ signals = m.get("signals", [])
135
+ warn_sigs = [s["label"] for s in signals if s.get("status") == "warn"]
136
+ diff --git a/sensei/server.py b/sensei/server.py
137
+ index c25f87c..7a1d190 100644
138
+ --- a/sensei/server.py
139
+ +++ b/sensei/server.py
140
+ @@ -83,21 +83,39 @@ class _Handler(BaseHTTPRequestHandler):
141
+ profile["graph"] = _json.loads(graph_file.read_text(encoding="utf-8"))
142
+ self._send(200, _json.dumps(profile, ensure_ascii=False).encode("utf-8"))
143
+ except Exception as e:
144
+ self._send(500, json.dumps({"error": str(e)}).encode())
145
+ # ── /api/llm/* — generación LLM on-demand con fallback ───────────────
146
+ elif self.path.startswith("/api/llm/"):
147
+ try:
148
+ from . import llm_coach
149
+ parsed = urllib.parse.urlparse(self.path)
150
+ qs = urllib.parse.parse_qs(parsed.query)
151
+ - endpoint = parsed.path # e.g. /api/llm/recap
152
+ + endpoint = parsed.path
153
+ +
154
+ + if qs.get("refresh", ["0"])[0] == "1":
155
+ + _BUCKET_MAP = {
156
+ + "/api/llm/narrative": "narrative",
157
+ + "/api/llm/recap": "recap",
158
+ + "/api/llm/roi_analysis": "roi",
159
+ + "/api/llm/coach_plan": "coach_plan",
160
+ + "/api/llm/briefing": "briefing",
161
+ + "/api/llm/anomaly": "anomaly",
162
+ + "/api/llm/insight": "insight",
163
+ + "/api/llm/claudemd": "claudemd",
164
+ + "/api/llm/skill": "skill",
165
+ + "/api/llm/profile_narrative": "profile_narrative",
166
+ + "/api/llm/claudemd_global": "claudemd_global",
167
+ + "/api/llm/node_explain": "node_explain",
168
+ + }
169
+ + if endpoint in _BUCKET_MAP:
170
+ + llm_coach.invalidate_bucket(_BUCKET_MAP[endpoint])
171
+
172
+ payload = build_payload(self.cfg)
173
+
174
+ if endpoint == "/api/llm/narrative":
175
+ result = llm_coach.generate(payload)
176
+ self._send(200, json.dumps(result, ensure_ascii=False).encode("utf-8"))
177
+
178
+ elif endpoint == "/api/llm/recap":
179
+ result = llm_coach.weekly_recap(payload)
180
+ self._send(200, json.dumps(result, ensure_ascii=False).encode("utf-8"))
181
+ diff --git a/sensei/ui/index.html b/sensei/ui/index.html
182
+ index 748bbc7..e1989aa 100644
183
+ --- a/sensei/ui/index.html
184
+ +++ b/sensei/ui/index.html
185
+ @@ -1392,43 +1392,20 @@ function loadLLMNodeExplain(btn) {
186
+ btn.textContent = '¿Qué dice esto de mí?';
187
+ btn.disabled = false;
188
+ })
189
+ .catch(() => {
190
+ if (panel) { panel.classList.remove('open'); panel.innerHTML = ''; }
191
+ btn.textContent = '¿Qué dice esto de mí?';
192
+ btn.disabled = false;
193
+ });
194
+ }
195
+
196
+ -// ── Interpretación del scatter ROI ────────────────────────────────────────
197
+ -let _roiAnalysisLoaded = false;
198
+ -function loadLLMRoiAnalysis() {
199
+ - if (_roiAnalysisLoaded) return;
200
+ - const block = document.getElementById('roi-llm-block');
201
+ - const textEl = document.getElementById('roi-llm-text');
202
+ - const srcEl = document.getElementById('roi-llm-src');
203
+ - if (!block || !textEl) return;
204
+ - block.style.display = 'block';
205
+ - textEl.innerHTML = _llmGenHtml('Interpretando gráfico…');
206
+ - fetch('/api/llm/roi_analysis')
207
+ - .then(r => r.ok ? r.json() : null)
208
+ - .then(result => {
209
+ - if (result?.text) {
210
+ - textEl.textContent = result.text;
211
+ - if (srcEl) srcEl.textContent = result.cli ? `Generado con ${result.cli}` : '';
212
+ - _roiAnalysisLoaded = true;
213
+ - } else {
214
+ - block.style.display = 'none';
215
+ - }
216
+ - })
217
+ - .catch(() => { block.style.display = 'none'; });
218
+ -}
219
+ // ── CLAUDE.md por proyecto (Proyectos tab) ────────────────────────────────
220
+ function loadLLMClaudeMd(btn) {
221
+ const project = btn.dataset.project;
222
+ const sessions = btn.dataset.sessions || 1;
223
+ const cost = btn.dataset.cost || 0;
224
+ const panel = btn.closest('.proj-llm-wrap').querySelector('.llm-claudemd-panel');
225
+ if (panel.classList.contains('open')) { panel.classList.remove('open'); panel.innerHTML = ''; btn.textContent = 'CLAUDE.md sugerido'; return; }
226
+ btn.textContent = 'Generando…';
227
+ btn.disabled = true;
228
+ panel.innerHTML = _llmGenHtml('Generando CLAUDE.md…');
@@ -0,0 +1,7 @@
1
+ # Review package: 183607c0df33de386fc636b8214a28c1383db0c0..HEAD
2
+
3
+ ## Commits
4
+
5
+ ## Files changed
6
+
7
+ ## Diff