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.
- onesensei-0.2.0/.claude/settings.json +5 -0
- onesensei-0.2.0/.github/workflows/release.yml +60 -0
- onesensei-0.2.0/.gitignore +15 -0
- onesensei-0.2.0/.graphifyignore +7 -0
- onesensei-0.2.0/.superpowers/sdd/.gitignore +1 -0
- onesensei-0.2.0/.superpowers/sdd/final-review-fix-report.md +106 -0
- onesensei-0.2.0/.superpowers/sdd/progress-org-admin-dashboard-poc-DONE.md +25 -0
- onesensei-0.2.0/.superpowers/sdd/progress-sensei-pro-licensing-DONE.md +22 -0
- onesensei-0.2.0/.superpowers/sdd/progress-work-item-cost-tracking-DONE.md +20 -0
- onesensei-0.2.0/.superpowers/sdd/progress.md +21 -0
- onesensei-0.2.0/.superpowers/sdd/review-013ee21..8157611.diff +118 -0
- onesensei-0.2.0/.superpowers/sdd/review-170bf5c..01d3cd7.diff +228 -0
- onesensei-0.2.0/.superpowers/sdd/review-183607c..183607c.diff +7 -0
- onesensei-0.2.0/.superpowers/sdd/review-1887fc0..28c543a.diff +158 -0
- onesensei-0.2.0/.superpowers/sdd/review-24d08b3..fa4f0c4.diff +212 -0
- onesensei-0.2.0/.superpowers/sdd/review-282df05..dec136b.diff +263 -0
- onesensei-0.2.0/.superpowers/sdd/review-28c543a..60d140c.diff +100 -0
- onesensei-0.2.0/.superpowers/sdd/review-3250b4d..65308ec.diff +638 -0
- onesensei-0.2.0/.superpowers/sdd/review-3250b4d..cbec820.diff +87 -0
- onesensei-0.2.0/.superpowers/sdd/review-45c9c18..6c9cdf4.diff +140 -0
- onesensei-0.2.0/.superpowers/sdd/review-4812860..HEAD.diff +210 -0
- onesensei-0.2.0/.superpowers/sdd/review-4f34233..HEAD.diff +110 -0
- onesensei-0.2.0/.superpowers/sdd/review-5c7e007..e8275b1.diff +199 -0
- onesensei-0.2.0/.superpowers/sdd/review-5ecc523..282df05.diff +190 -0
- onesensei-0.2.0/.superpowers/sdd/review-5ecc523..c2a821c.diff +600 -0
- onesensei-0.2.0/.superpowers/sdd/review-60d140c..013ee21.diff +391 -0
- onesensei-0.2.0/.superpowers/sdd/review-62e4c91..0048df8.diff +123 -0
- onesensei-0.2.0/.superpowers/sdd/review-62e4c91..183607c.diff +361 -0
- onesensei-0.2.0/.superpowers/sdd/review-62e4c91..3250b4d.diff +79 -0
- onesensei-0.2.0/.superpowers/sdd/review-6f9d6f1..45c9c18.diff +155 -0
- onesensei-0.2.0/.superpowers/sdd/review-6f9d6f1..6c9cdf4.diff +245 -0
- onesensei-0.2.0/.superpowers/sdd/review-713bbe2..7b2bd75.diff +194 -0
- onesensei-0.2.0/.superpowers/sdd/review-78bd84a..1887fc0.diff +150 -0
- onesensei-0.2.0/.superpowers/sdd/review-7b2bd75..c2a821c.diff +164 -0
- onesensei-0.2.0/.superpowers/sdd/review-8157611..06d13d6.diff +296 -0
- onesensei-0.2.0/.superpowers/sdd/review-898ccb7..6f9d6f1.diff +649 -0
- onesensei-0.2.0/.superpowers/sdd/review-898ccb7..c74153c.diff +254 -0
- onesensei-0.2.0/.superpowers/sdd/review-90311d1..ba4c57a.diff +859 -0
- onesensei-0.2.0/.superpowers/sdd/review-911b46d..5ecc523.diff +92 -0
- onesensei-0.2.0/.superpowers/sdd/review-97355a1..6f9d6f1.diff +154 -0
- onesensei-0.2.0/.superpowers/sdd/review-99b5d80..8157611.diff +2072 -0
- onesensei-0.2.0/.superpowers/sdd/review-99b5d80..90311d1.diff +234 -0
- onesensei-0.2.0/.superpowers/sdd/review-aa41f1e..24d08b3.diff +53 -0
- onesensei-0.2.0/.superpowers/sdd/review-ac0ee24..08a676b.diff +435 -0
- onesensei-0.2.0/.superpowers/sdd/review-ac0ee24..f452411.diff +66 -0
- onesensei-0.2.0/.superpowers/sdd/review-ba4c57a..78bd84a.diff +390 -0
- onesensei-0.2.0/.superpowers/sdd/review-c2a821c..898ccb7.diff +122 -0
- onesensei-0.2.0/.superpowers/sdd/review-c74153c..5c7e007.diff +119 -0
- onesensei-0.2.0/.superpowers/sdd/review-cbec820..183607c.diff +216 -0
- onesensei-0.2.0/.superpowers/sdd/review-d2d6546..01d3cd7.diff +607 -0
- onesensei-0.2.0/.superpowers/sdd/review-d2d6546..399182c.diff +376 -0
- onesensei-0.2.0/.superpowers/sdd/review-d2d6546..ac0ee24.diff +281 -0
- onesensei-0.2.0/.superpowers/sdd/review-dec136b..713bbe2.diff +51 -0
- onesensei-0.2.0/.superpowers/sdd/review-f452411..170bf5c.diff +182 -0
- onesensei-0.2.0/.superpowers/sdd/review-f452411..8b3b7b2.diff +707 -0
- onesensei-0.2.0/.superpowers/sdd/review-fa4f0c4..cfa560a.diff +178 -0
- onesensei-0.2.0/.superpowers/sdd/review-final.diff +545 -0
- onesensei-0.2.0/.superpowers/sdd/review-task2-only.diff +126 -0
- onesensei-0.2.0/.superpowers/sdd/review-task3.diff +56 -0
- onesensei-0.2.0/.superpowers/sdd/task-1-brief.md +129 -0
- onesensei-0.2.0/.superpowers/sdd/task-1-fix-report.md +138 -0
- onesensei-0.2.0/.superpowers/sdd/task-1-report.md +109 -0
- onesensei-0.2.0/.superpowers/sdd/task-2-brief.md +126 -0
- onesensei-0.2.0/.superpowers/sdd/task-2-fix-report.md +106 -0
- onesensei-0.2.0/.superpowers/sdd/task-2-report.md +52 -0
- onesensei-0.2.0/.superpowers/sdd/task-3-brief.md +211 -0
- onesensei-0.2.0/.superpowers/sdd/task-3-fix-report.md +119 -0
- onesensei-0.2.0/.superpowers/sdd/task-3-report.md +121 -0
- onesensei-0.2.0/.superpowers/sdd/task-4-brief.md +158 -0
- onesensei-0.2.0/.superpowers/sdd/task-4-fix-report.md +146 -0
- onesensei-0.2.0/.superpowers/sdd/task-4-report.md +97 -0
- onesensei-0.2.0/.superpowers/sdd/task-5-brief.md +283 -0
- onesensei-0.2.0/.superpowers/sdd/task-5-report.md +105 -0
- onesensei-0.2.0/.superpowers/sdd/task-6-brief.md +88 -0
- onesensei-0.2.0/.superpowers/sdd/task-6-dojo-report.md +117 -0
- onesensei-0.2.0/.superpowers/sdd/task-6-report.md +62 -0
- onesensei-0.2.0/.superpowers/sdd/task-7-report.md +187 -0
- onesensei-0.2.0/CHANGELOG.md +39 -0
- onesensei-0.2.0/CLAUDE.md +59 -0
- onesensei-0.2.0/PKG-INFO +516 -0
- onesensei-0.2.0/README.md +501 -0
- onesensei-0.2.0/docs/ROADMAP_SPEC.md +272 -0
- onesensei-0.2.0/docs/sensei v2.md +383 -0
- onesensei-0.2.0/docs/superpowers/plans/2026-06-27-backlog-condicional.md +924 -0
- onesensei-0.2.0/docs/superpowers/plans/2026-06-28-ai-maturity-insights.md +1042 -0
- onesensei-0.2.0/docs/superpowers/plans/2026-06-28-developer-memory.md +1484 -0
- onesensei-0.2.0/docs/superpowers/plans/2026-06-29-briefing-into-resumen.md +287 -0
- onesensei-0.2.0/docs/superpowers/plans/2026-06-29-coach-insights-frontend.md +369 -0
- onesensei-0.2.0/docs/superpowers/plans/2026-06-29-llm-perfil-tab.md +610 -0
- onesensei-0.2.0/docs/superpowers/plans/2026-07-05-dojo-global-admin-cross-tenant.md +1022 -0
- onesensei-0.2.0/docs/superpowers/plans/2026-07-05-dojo-license-service.md +1124 -0
- onesensei-0.2.0/docs/superpowers/plans/2026-07-05-org-admin-dashboard-poc.md +774 -0
- onesensei-0.2.0/docs/superpowers/plans/2026-07-05-pro-lock-ui.md +275 -0
- onesensei-0.2.0/docs/superpowers/plans/2026-07-05-sensei-pro-licensing-client.md +1436 -0
- onesensei-0.2.0/docs/superpowers/plans/2026-07-05-work-item-cost-tracking.md +577 -0
- onesensei-0.2.0/docs/superpowers/specs/2026-06-27-backlog-condicional-design.md +85 -0
- onesensei-0.2.0/docs/superpowers/specs/2026-06-28-ai-maturity-insights.md +142 -0
- onesensei-0.2.0/docs/superpowers/specs/2026-06-28-developer-memory-design.md +277 -0
- onesensei-0.2.0/docs/superpowers/specs/2026-06-29-briefing-into-resumen.md +134 -0
- onesensei-0.2.0/docs/superpowers/specs/2026-06-29-coach-insights-frontend.md +160 -0
- onesensei-0.2.0/docs/superpowers/specs/2026-06-29-llm-perfil-tab.md +167 -0
- onesensei-0.2.0/docs/superpowers/specs/2026-07-05-dojo-global-admin-cross-tenant.md +117 -0
- onesensei-0.2.0/docs/superpowers/specs/2026-07-05-sensei-pro-tiering-design.md +162 -0
- onesensei-0.2.0/dojo/.dockerignore +9 -0
- onesensei-0.2.0/dojo/.env.example +1 -0
- onesensei-0.2.0/dojo/Dockerfile +30 -0
- onesensei-0.2.0/dojo/README.md +78 -0
- onesensei-0.2.0/dojo/dojo/__init__.py +0 -0
- onesensei-0.2.0/dojo/dojo/admin_cli.py +134 -0
- onesensei-0.2.0/dojo/dojo/app.py +60 -0
- onesensei-0.2.0/dojo/dojo/db.py +92 -0
- onesensei-0.2.0/dojo/dojo/security.py +85 -0
- onesensei-0.2.0/dojo/pyproject.toml +27 -0
- onesensei-0.2.0/dojo/tests/__init__.py +0 -0
- onesensei-0.2.0/dojo/tests/conftest.py +6 -0
- onesensei-0.2.0/dojo/tests/test_admin_cli.py +88 -0
- onesensei-0.2.0/dojo/tests/test_app.py +77 -0
- onesensei-0.2.0/dojo/tests/test_db.py +72 -0
- onesensei-0.2.0/dojo/tests/test_security.py +70 -0
- onesensei-0.2.0/install.sh +36 -0
- onesensei-0.2.0/pyproject.toml +45 -0
- onesensei-0.2.0/scripts/graph_context.py +93 -0
- onesensei-0.2.0/sensei/__init__.py +2 -0
- onesensei-0.2.0/sensei/_resources.py +13 -0
- onesensei-0.2.0/sensei/adapters.py +169 -0
- onesensei-0.2.0/sensei/baseline.json +5 -0
- onesensei-0.2.0/sensei/cli.py +313 -0
- onesensei-0.2.0/sensei/coach.py +447 -0
- onesensei-0.2.0/sensei/collectors/__init__.py +0 -0
- onesensei-0.2.0/sensei/collectors/claude_code.py +437 -0
- onesensei-0.2.0/sensei/collectors/copilot.py +278 -0
- onesensei-0.2.0/sensei/collectors/git_status.py +93 -0
- onesensei-0.2.0/sensei/collectors/kiro.py +429 -0
- onesensei-0.2.0/sensei/collectors/opencode.py +501 -0
- onesensei-0.2.0/sensei/config.default.json +98 -0
- onesensei-0.2.0/sensei/config.py +120 -0
- onesensei-0.2.0/sensei/content_parser.py +493 -0
- onesensei-0.2.0/sensei/costo.py +251 -0
- onesensei-0.2.0/sensei/engine.py +784 -0
- onesensei-0.2.0/sensei/graph.py +127 -0
- onesensei-0.2.0/sensei/licensing.py +219 -0
- onesensei-0.2.0/sensei/live.py +280 -0
- onesensei-0.2.0/sensei/llm_coach.py +980 -0
- onesensei-0.2.0/sensei/maturity.py +303 -0
- onesensei-0.2.0/sensei/morning.py +296 -0
- onesensei-0.2.0/sensei/profiler.py +173 -0
- onesensei-0.2.0/sensei/roi.py +228 -0
- onesensei-0.2.0/sensei/server.py +783 -0
- onesensei-0.2.0/sensei/setup_wizard.py +143 -0
- onesensei-0.2.0/sensei/ui/index.html +3869 -0
- onesensei-0.2.0/sensei/ui/live.html +121 -0
- onesensei-0.2.0/sensei/ui/settings.html +1029 -0
- onesensei-0.2.0/sensei/ui/setup.html +187 -0
- onesensei-0.2.0/sensei/voice.py +55 -0
- onesensei-0.2.0/sensei.spec +70 -0
- onesensei-0.2.0/tests/__init__.py +0 -0
- onesensei-0.2.0/tests/fixtures.py +61 -0
- onesensei-0.2.0/tests/integration/__init__.py +0 -0
- onesensei-0.2.0/tests/integration/test_sensei_dojo_interop.py +47 -0
- onesensei-0.2.0/tests/smoke.py +597 -0
- onesensei-0.2.0/tests/test_adapters.py +78 -0
- onesensei-0.2.0/tests/test_burn.py +55 -0
- onesensei-0.2.0/tests/test_cli_ascii.py +34 -0
- onesensei-0.2.0/tests/test_cli_costo.py +135 -0
- onesensei-0.2.0/tests/test_cli_dojo.py +42 -0
- onesensei-0.2.0/tests/test_cli_insights.py +59 -0
- onesensei-0.2.0/tests/test_cli_profile_licensing_gate.py +19 -0
- onesensei-0.2.0/tests/test_client_budgets.py +57 -0
- onesensei-0.2.0/tests/test_coach_backlog.py +35 -0
- onesensei-0.2.0/tests/test_content_parser.py +88 -0
- onesensei-0.2.0/tests/test_costo.py +367 -0
- onesensei-0.2.0/tests/test_engine_backlog.py +44 -0
- onesensei-0.2.0/tests/test_engine_licensing_gate.py +54 -0
- onesensei-0.2.0/tests/test_graph.py +78 -0
- onesensei-0.2.0/tests/test_licensing_core.py +171 -0
- onesensei-0.2.0/tests/test_licensing_network.py +261 -0
- onesensei-0.2.0/tests/test_live.py +113 -0
- onesensei-0.2.0/tests/test_llm_coach.py +70 -0
- onesensei-0.2.0/tests/test_maturity.py +381 -0
- onesensei-0.2.0/tests/test_opencode_parity.py +115 -0
- onesensei-0.2.0/tests/test_profile_cmd.py +55 -0
- onesensei-0.2.0/tests/test_profiler.py +72 -0
- onesensei-0.2.0/tests/test_repeated_context.py +65 -0
- onesensei-0.2.0/tests/test_roi_backlog.py +25 -0
- onesensei-0.2.0/tests/test_roi_total_value.py +51 -0
- onesensei-0.2.0/tests/test_server_apply.py +98 -0
- onesensei-0.2.0/tests/test_server_export.py +70 -0
- onesensei-0.2.0/tests/test_server_history.py +95 -0
- onesensei-0.2.0/tests/test_server_licensing_gate.py +101 -0
- onesensei-0.2.0/tests/test_session_autopsy.py +58 -0
- onesensei-0.2.0/tests/test_setup_wizard.py +106 -0
- onesensei-0.2.0/tests/test_smoke_ui.py +187 -0
- onesensei-0.2.0/tests/test_whatif.py +85 -0
- onesensei-0.2.0/uv.lock +987 -0
|
@@ -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 @@
|
|
|
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…');
|