agentpack-cli 0.3.13__tar.gz → 0.3.14__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 (139) hide show
  1. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/PKG-INFO +11 -5
  2. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/README.md +10 -4
  3. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/pyproject.toml +1 -1
  4. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/__init__.py +1 -1
  5. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/application/pack_service.py +2 -0
  6. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/benchmark.py +22 -0
  7. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/dev_check.py +14 -1
  8. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/init.py +4 -0
  9. agentpack_cli-0.3.14/src/agentpack/commands/learn.py +179 -0
  10. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/release_check.py +16 -1
  11. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/config.py +8 -0
  12. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/learning/__init__.py +4 -0
  13. agentpack_cli-0.3.14/src/agentpack/learning/feedback.py +171 -0
  14. agentpack_cli-0.3.14/src/agentpack/learning/lesson_ranker.py +39 -0
  15. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/learning/models.py +34 -0
  16. agentpack_cli-0.3.14/src/agentpack/learning/provider.py +52 -0
  17. agentpack_cli-0.3.14/src/agentpack/learning/renderers.py +235 -0
  18. agentpack_cli-0.3.14/src/agentpack/learning/skill_map.py +142 -0
  19. agentpack_cli-0.3.13/src/agentpack/commands/learn.py +0 -101
  20. agentpack_cli-0.3.13/src/agentpack/learning/feedback.py +0 -22
  21. agentpack_cli-0.3.13/src/agentpack/learning/renderers.py +0 -99
  22. agentpack_cli-0.3.13/src/agentpack/learning/skill_map.py +0 -29
  23. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/.gitignore +0 -0
  24. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/LICENSE +0 -0
  25. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/adapters/__init__.py +0 -0
  26. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/adapters/antigravity.py +0 -0
  27. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/adapters/base.py +0 -0
  28. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/adapters/claude.py +0 -0
  29. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/adapters/codex.py +0 -0
  30. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/adapters/cursor.py +0 -0
  31. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/adapters/detect.py +0 -0
  32. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/adapters/generic.py +0 -0
  33. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/adapters/windsurf.py +0 -0
  34. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/__init__.py +0 -0
  35. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/dependency_graph.py +0 -0
  36. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/go_imports.py +0 -0
  37. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/java_imports.py +0 -0
  38. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/js_ts_imports.py +0 -0
  39. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/monorepo.py +0 -0
  40. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/naming_signals.py +0 -0
  41. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/python_imports.py +0 -0
  42. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/ranking.py +0 -0
  43. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/repo_map.py +0 -0
  44. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/role_inference.py +0 -0
  45. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/rust_imports.py +0 -0
  46. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/symbols.py +0 -0
  47. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/task_classifier.py +0 -0
  48. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/analysis/tests.py +0 -0
  49. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/application/__init__.py +0 -0
  50. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/cli.py +0 -0
  51. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/__init__.py +0 -0
  52. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/_shared.py +0 -0
  53. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/ci_cmd.py +0 -0
  54. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/claude_cmd.py +0 -0
  55. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/diagnose_selection.py +0 -0
  56. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/diff.py +0 -0
  57. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/doctor.py +0 -0
  58. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/eval_cmd.py +0 -0
  59. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/explain.py +0 -0
  60. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/guard.py +0 -0
  61. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/hook_cmd.py +0 -0
  62. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/ignore_cmd.py +0 -0
  63. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/install.py +0 -0
  64. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/mcp_cmd.py +0 -0
  65. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/migrate.py +0 -0
  66. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/monitor.py +0 -0
  67. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/next_cmd.py +0 -0
  68. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/pack.py +0 -0
  69. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/quickstart.py +0 -0
  70. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/release_cmd.py +0 -0
  71. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/repair.py +0 -0
  72. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/route.py +0 -0
  73. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/scan.py +0 -0
  74. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/skills.py +0 -0
  75. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/start_cmd.py +0 -0
  76. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/state_cmd.py +0 -0
  77. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/stats.py +0 -0
  78. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/status.py +0 -0
  79. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/summarize.py +0 -0
  80. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/task_cmd.py +0 -0
  81. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/threads.py +0 -0
  82. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/tune.py +0 -0
  83. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/verify_wheel.py +0 -0
  84. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/watch.py +0 -0
  85. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/commands/workflow_cmd.py +0 -0
  86. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/__init__.py +0 -0
  87. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/bootstrap.py +0 -0
  88. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/cache.py +0 -0
  89. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/changed_paths.py +0 -0
  90. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/context_pack.py +0 -0
  91. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/diff.py +0 -0
  92. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/evals.py +0 -0
  93. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/execution_state.py +0 -0
  94. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/git.py +0 -0
  95. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/git_hooks.py +0 -0
  96. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/global_install.py +0 -0
  97. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/ignore.py +0 -0
  98. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/merkle.py +0 -0
  99. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/models.py +0 -0
  100. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/redactor.py +0 -0
  101. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/scanner.py +0 -0
  102. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/snapshot.py +0 -0
  103. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/task_freshness.py +0 -0
  104. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/thread_context.py +0 -0
  105. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/token_estimator.py +0 -0
  106. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/core/vscode_tasks.py +0 -0
  107. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/data/agentpack.md +0 -0
  108. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/installers/__init__.py +0 -0
  109. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/installers/antigravity.py +0 -0
  110. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/installers/claude.py +0 -0
  111. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/installers/codex.py +0 -0
  112. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/installers/cursor.py +0 -0
  113. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/installers/windsurf.py +0 -0
  114. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/integrations/__init__.py +0 -0
  115. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/integrations/agents.py +0 -0
  116. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/integrations/git_hooks.py +0 -0
  117. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/integrations/global_install.py +0 -0
  118. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/integrations/platform.py +0 -0
  119. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/integrations/vscode_tasks.py +0 -0
  120. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/learning/collector.py +0 -0
  121. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/learning/extractor.py +0 -0
  122. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/learning/quality.py +0 -0
  123. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/mcp_server.py +0 -0
  124. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/renderers/__init__.py +0 -0
  125. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/renderers/compact.py +0 -0
  126. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/renderers/markdown.py +0 -0
  127. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/renderers/receipts.py +0 -0
  128. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/router/__init__.py +0 -0
  129. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/router/discovery.py +0 -0
  130. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/router/models.py +0 -0
  131. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/router/parser.py +0 -0
  132. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/router/prompt_builder.py +0 -0
  133. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/router/scoring.py +0 -0
  134. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/router/service.py +0 -0
  135. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/session/__init__.py +0 -0
  136. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/session/state.py +0 -0
  137. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/summaries/__init__.py +0 -0
  138. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/summaries/base.py +0 -0
  139. {agentpack_cli-0.3.13 → agentpack_cli-0.3.14}/src/agentpack/summaries/offline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentpack-cli
3
- Version: 0.3.13
3
+ Version: 0.3.14
4
4
  Summary: Local MCP context router for Claude Code, Codex, Cursor, and AI coding agents.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -63,7 +63,7 @@ pipx run --spec agentpack-cli agentpack route --task "fix auth token expiry"
63
63
 
64
64
  ![AgentPack route demo](docs/assets/agentpack-route-demo.svg)
65
65
 
66
- > **Status: alpha (v0.3.13).** Works, tested, and used in real sessions. Python and JavaScript/TypeScript are the best-supported languages. Current benchmarks are useful regression checks, not broad proof that AgentPack improves coding-agent success. API may change before 1.0.
66
+ > **Status: alpha (v0.3.14).** Works, tested, and used in real sessions. Python and JavaScript/TypeScript are the best-supported languages. Current benchmarks are useful regression checks, not broad proof that AgentPack improves coding-agent success. API may change before 1.0.
67
67
  >
68
68
  > **Platform note:** macOS, Linux, and Windows are supported. Windows support targets PowerShell plus Git for Windows. `cmd.exe` and bare Git setups are not a supported path yet.
69
69
  >
@@ -205,10 +205,16 @@ agentpack learn --today
205
205
  agentpack learn --since main
206
206
  agentpack learn --json
207
207
  agentpack learn --llm-prompt --pr-comment
208
- agentpack learn --feedback helpful --feedback-note "Useful review prompts"
208
+ agentpack learn --provider-preview
209
+ agentpack learn --provider-command "python scripts/learn_provider.py"
210
+ agentpack learn --dashboard --team-export
211
+ agentpack learn --skills
212
+ agentpack learn --drills
213
+ agentpack learn --ci
214
+ agentpack learn --feedback helpful --feedback-target "skill:CLI design" --feedback-note "Useful review prompts"
209
215
  ```
210
216
 
211
- AgentPack writes developer notes to `.agentpack/learning.md` or `.agentpack/daily-summary.md`, updates `.agentpack/skills-progress.json`, writes `.agentpack/agent-lessons.md` for future coding agents, and can emit `.agentpack/learning.prompt.md` or `.agentpack/pr-learning-comment.md`. The MVP is local-first and reuses AgentPack redaction before reading diff snippets.
217
+ AgentPack writes developer notes to `.agentpack/learning.md` or `.agentpack/daily-summary.md`, updates a local skill memory in `.agentpack/skills-progress.json`, writes ranked `.agentpack/agent-lessons.md` for future coding agents, and can emit `.agentpack/learning.prompt.md`, `.agentpack/pr-learning-comment.md`, `.agentpack/learning-dashboard.html`, or `.agentpack/team-lessons.md`. Learn is local-first by default: `--provider-preview` shows the bounded payload for optional external refinement without making a network call, `--provider-command` runs only the local command you provide, and feedback stays in `.agentpack/learning-feedback.jsonl`.
212
218
 
213
219
  ## Agent Setup
214
220
 
@@ -319,7 +325,7 @@ gate.
319
325
  | `agentpack work "task"` | Initialize if needed, start task, refresh context, show next steps |
320
326
  | `agentpack start "task"` | Write task and run the guard/refresh workflow |
321
327
  | `agentpack finish --since main` | Diagnose, capture benchmark case, run checks, mark done |
322
- | `agentpack learn` | Generate developer learning notes, skill progress, and future-agent lessons |
328
+ | `agentpack learn` | Generate developer learning notes, skill memory, feedback-aware drills, and future-agent lessons |
323
329
  | `agentpack task show|set|clear` | Manage global or thread-scoped task files |
324
330
  | `agentpack pack` | Generate a ranked context pack for `.agentpack/task.md` |
325
331
  | `agentpack next --fix-all-safe` | Ask AgentPack what command or safe repair should happen next |
@@ -24,7 +24,7 @@ pipx run --spec agentpack-cli agentpack route --task "fix auth token expiry"
24
24
 
25
25
  ![AgentPack route demo](docs/assets/agentpack-route-demo.svg)
26
26
 
27
- > **Status: alpha (v0.3.13).** Works, tested, and used in real sessions. Python and JavaScript/TypeScript are the best-supported languages. Current benchmarks are useful regression checks, not broad proof that AgentPack improves coding-agent success. API may change before 1.0.
27
+ > **Status: alpha (v0.3.14).** Works, tested, and used in real sessions. Python and JavaScript/TypeScript are the best-supported languages. Current benchmarks are useful regression checks, not broad proof that AgentPack improves coding-agent success. API may change before 1.0.
28
28
  >
29
29
  > **Platform note:** macOS, Linux, and Windows are supported. Windows support targets PowerShell plus Git for Windows. `cmd.exe` and bare Git setups are not a supported path yet.
30
30
  >
@@ -166,10 +166,16 @@ agentpack learn --today
166
166
  agentpack learn --since main
167
167
  agentpack learn --json
168
168
  agentpack learn --llm-prompt --pr-comment
169
- agentpack learn --feedback helpful --feedback-note "Useful review prompts"
169
+ agentpack learn --provider-preview
170
+ agentpack learn --provider-command "python scripts/learn_provider.py"
171
+ agentpack learn --dashboard --team-export
172
+ agentpack learn --skills
173
+ agentpack learn --drills
174
+ agentpack learn --ci
175
+ agentpack learn --feedback helpful --feedback-target "skill:CLI design" --feedback-note "Useful review prompts"
170
176
  ```
171
177
 
172
- AgentPack writes developer notes to `.agentpack/learning.md` or `.agentpack/daily-summary.md`, updates `.agentpack/skills-progress.json`, writes `.agentpack/agent-lessons.md` for future coding agents, and can emit `.agentpack/learning.prompt.md` or `.agentpack/pr-learning-comment.md`. The MVP is local-first and reuses AgentPack redaction before reading diff snippets.
178
+ AgentPack writes developer notes to `.agentpack/learning.md` or `.agentpack/daily-summary.md`, updates a local skill memory in `.agentpack/skills-progress.json`, writes ranked `.agentpack/agent-lessons.md` for future coding agents, and can emit `.agentpack/learning.prompt.md`, `.agentpack/pr-learning-comment.md`, `.agentpack/learning-dashboard.html`, or `.agentpack/team-lessons.md`. Learn is local-first by default: `--provider-preview` shows the bounded payload for optional external refinement without making a network call, `--provider-command` runs only the local command you provide, and feedback stays in `.agentpack/learning-feedback.jsonl`.
173
179
 
174
180
  ## Agent Setup
175
181
 
@@ -280,7 +286,7 @@ gate.
280
286
  | `agentpack work "task"` | Initialize if needed, start task, refresh context, show next steps |
281
287
  | `agentpack start "task"` | Write task and run the guard/refresh workflow |
282
288
  | `agentpack finish --since main` | Diagnose, capture benchmark case, run checks, mark done |
283
- | `agentpack learn` | Generate developer learning notes, skill progress, and future-agent lessons |
289
+ | `agentpack learn` | Generate developer learning notes, skill memory, feedback-aware drills, and future-agent lessons |
284
290
  | `agentpack task show|set|clear` | Manage global or thread-scoped task files |
285
291
  | `agentpack pack` | Generate a ranked context pack for `.agentpack/task.md` |
286
292
  | `agentpack next --fix-all-safe` | Ask AgentPack what command or safe repair should happen next |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentpack-cli"
3
- version = "0.3.13"
3
+ version = "0.3.14"
4
4
  description = "Local MCP context router for Claude Code, Codex, Cursor, and AI coding agents."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """AgentPack — task-aware context packing for AI coding agents."""
2
2
 
3
- __version__ = "0.3.13"
3
+ __version__ = "0.3.14"
@@ -660,6 +660,8 @@ class AdapterRegistry:
660
660
  cfg.learning.agent_lessons_output,
661
661
  cfg.learning.llm_prompt_output,
662
662
  cfg.learning.pr_comment_output,
663
+ cfg.learning.dashboard_output,
664
+ cfg.learning.team_lessons_output,
663
665
  cfg.learning.feedback_output,
664
666
  }
665
667
  )
@@ -469,6 +469,26 @@ def _run_git(cwd: Path | None, args: list[str]) -> None:
469
469
  )
470
470
 
471
471
 
472
+ def _git_commit_exists(cwd: Path, commit: str) -> bool:
473
+ result = subprocess.run(
474
+ ["git", "cat-file", "-e", f"{commit}^{{commit}}"],
475
+ cwd=cwd,
476
+ text=True,
477
+ stdout=subprocess.PIPE,
478
+ stderr=subprocess.PIPE,
479
+ check=False,
480
+ )
481
+ return result.returncode == 0
482
+
483
+
484
+ def _ensure_git_commit(cwd: Path, commit: str) -> None:
485
+ if _git_commit_exists(cwd, commit):
486
+ return
487
+ _run_git(cwd, ["fetch", "--quiet", "--depth", "1", "origin", commit])
488
+ if not _git_commit_exists(cwd, commit):
489
+ raise RuntimeError(f"Unable to fetch public benchmark commit {commit}")
490
+
491
+
472
492
  def _ensure_public_repo_clone(
473
493
  spec: PublicRepoSpec,
474
494
  cache_dir: Path,
@@ -512,7 +532,9 @@ def _run_public_repo_suite(
512
532
  for spec in specs:
513
533
  source_repo = _ensure_public_repo_clone(spec, cache, refresh=refresh)
514
534
  for public_case in spec.cases:
535
+ _ensure_git_commit(source_repo, public_case.commit)
515
536
  parent = _git_stdout(source_repo, ["rev-parse", f"{public_case.commit}^"])
537
+ _ensure_git_commit(source_repo, parent)
516
538
  work_root = temp_root / f"{spec.name}-{public_case.commit[:8]}"
517
539
  shutil.copytree(
518
540
  source_repo,
@@ -40,6 +40,8 @@ def register(app: typer.Typer) -> None:
40
40
  marker = "[green]✓[/]" if item["returncode"] == 0 else "[red]✗[/]"
41
41
  console.print(f"{marker} {item['name']} ({item['duration_s']:.2f}s)")
42
42
  if item["returncode"] != 0:
43
+ if item["output_excerpt"]:
44
+ console.print(item["output_excerpt"])
43
45
  console.print(f" rerun: [bold]{item['command']}[/]")
44
46
  if failed:
45
47
  raise typer.Exit(1)
@@ -48,10 +50,21 @@ def register(app: typer.Typer) -> None:
48
50
  def _run(stage: CheckStage) -> dict[str, Any]:
49
51
  started = time.perf_counter()
50
52
  result = subprocess.run(stage.command, cwd=_root(), capture_output=True, text=True)
53
+ output = (result.stdout + "\n" + result.stderr).strip()
51
54
  return {
52
55
  "name": stage.name,
53
56
  "command": " ".join(stage.command),
54
57
  "returncode": result.returncode,
55
58
  "duration_s": round(time.perf_counter() - started, 3),
56
- "detail": ((result.stderr or result.stdout).strip().splitlines() or [""])[-1],
59
+ "detail": (output.splitlines() or [""])[-1],
60
+ "output_excerpt": _output_excerpt(output) if result.returncode != 0 else "",
57
61
  }
62
+
63
+
64
+ def _output_excerpt(output: str, *, max_lines: int = 80) -> str:
65
+ lines = output.splitlines()
66
+ if len(lines) <= max_lines:
67
+ excerpt = lines
68
+ else:
69
+ excerpt = ["... output truncated to final failing lines ...", *lines[-max_lines:]]
70
+ return "\n".join(f" {line}" for line in excerpt)
@@ -68,6 +68,8 @@ def _repo_gitignore_entries(share_cache: bool = False, agent: str = "generic") -
68
68
  ".agentpack/agent-lessons.md",
69
69
  ".agentpack/learning.prompt.md",
70
70
  ".agentpack/pr-learning-comment.md",
71
+ ".agentpack/learning-dashboard.html",
72
+ ".agentpack/team-lessons.md",
71
73
  ".agentpack/learning-feedback.jsonl",
72
74
  ".agentignore",
73
75
  ]
@@ -107,6 +109,8 @@ def _agentpack_gitignore_content(share_cache: bool = False) -> str:
107
109
  "agent-lessons.md",
108
110
  "learning.prompt.md",
109
111
  "pr-learning-comment.md",
112
+ "learning-dashboard.html",
113
+ "team-lessons.md",
110
114
  "learning-feedback.jsonl",
111
115
  ]
112
116
  )
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import datetime
5
+
6
+ import typer
7
+
8
+ from agentpack.commands._shared import _atomic_write, _root, console
9
+ from agentpack.core.config import load_config
10
+ from agentpack.learning.collector import collect_learning_inputs
11
+ from agentpack.learning.extractor import build_learning_report
12
+ from agentpack.learning.feedback import apply_feedback_to_report, load_feedback_summary, record_learning_feedback
13
+ from agentpack.learning.lesson_ranker import rank_agent_lessons
14
+ from agentpack.learning.provider import LearningProviderError, run_provider_command
15
+ from agentpack.learning.quality import score_learning_report
16
+ from agentpack.learning.renderers import (
17
+ learning_report_to_dict,
18
+ render_dashboard_html,
19
+ render_agent_lessons_markdown,
20
+ render_drills_markdown,
21
+ render_llm_prompt_markdown,
22
+ render_learning_markdown,
23
+ render_pr_comment_markdown,
24
+ render_provider_preview_markdown,
25
+ render_quality_markdown,
26
+ render_team_lessons_markdown,
27
+ )
28
+ from agentpack.learning.skill_map import apply_skill_feedback, recommend_practice_drills, render_skill_summary, update_skill_map
29
+
30
+
31
+ def register(app: typer.Typer) -> None:
32
+ @app.command()
33
+ def learn(
34
+ task: str = typer.Option("auto", "--task", help="Task source. Only 'auto' is supported."),
35
+ since: str | None = typer.Option(None, "--since", help="Git ref to compare against, e.g. HEAD~1 or main."),
36
+ today: bool = typer.Option(False, "--today", help="Use today's work scope label for the report."),
37
+ output: str = typer.Option("", "--output", "-o", help="Markdown output path."),
38
+ json_output: bool = typer.Option(False, "--json", help="Print JSON to stdout instead of writing Markdown."),
39
+ llm_prompt: bool = typer.Option(False, "--llm-prompt", help="Write an LLM-ready learning prompt artifact."),
40
+ pr_comment: bool = typer.Option(False, "--pr-comment", help="Write a PR-comment-ready learning summary artifact."),
41
+ provider_preview: bool = typer.Option(False, "--provider-preview", help="Print the bounded provider payload without making a network call."),
42
+ provider_command: str = typer.Option("", "--provider-command", help="Run a local JSON-in/JSON-out provider command to enrich the report."),
43
+ dashboard: bool = typer.Option(False, "--dashboard", help="Write a static HTML learning dashboard artifact."),
44
+ team_export: bool = typer.Option(False, "--team-export", help="Write an opt-in team lesson export without personal skill history."),
45
+ ci: bool = typer.Option(False, "--ci", help="Fail when learning quality is below the configured threshold."),
46
+ skills: bool = typer.Option(False, "--skills", help="Print the local skill memory summary and exit."),
47
+ drills: bool = typer.Option(False, "--drills", help="Print recommended practice drills from local skill memory and exit."),
48
+ feedback: str = typer.Option("", "--feedback", help="Record feedback for this learning output (helpful|not-helpful)."),
49
+ feedback_note: str = typer.Option("", "--feedback-note", help="Optional note stored with --feedback."),
50
+ feedback_target: str = typer.Option("", "--feedback-target", help="Optional target such as skill:CLI design, lesson:retry, rename:old=>new, or merge:old=>new."),
51
+ suppress_skill: str = typer.Option("", "--suppress-skill", help="Suppress a noisy skill in future skill views and generation."),
52
+ rename_skill: str = typer.Option("", "--rename-skill", help="Rename a skill using old=>new."),
53
+ merge_skill: str = typer.Option("", "--merge-skill", help="Merge a skill using old=>new."),
54
+ ) -> None:
55
+ """Generate local learning artifacts from current task and git changes."""
56
+ if task != "auto":
57
+ console.print(
58
+ "[red]`agentpack learn --task \"...\"` is not supported. "
59
+ "Write .agentpack/task.md and use --task auto.[/]"
60
+ )
61
+ raise typer.Exit(2)
62
+
63
+ root = _root()
64
+ cfg = load_config(root)
65
+ skill_map_path = root / cfg.learning.skill_map_output
66
+ if skills:
67
+ typer.echo(render_skill_summary(skill_map_path), nl=False)
68
+ return
69
+ if drills:
70
+ typer.echo(render_drills_markdown(recommend_practice_drills(skill_map_path)), nl=False)
71
+ return
72
+ if suppress_skill:
73
+ apply_skill_feedback(skill_map_path, target=suppress_skill, action="suppress", note=feedback_note)
74
+ console.print(f"[green]✓[/] Suppressed skill {suppress_skill}")
75
+ return
76
+ if rename_skill:
77
+ old, new = _split_mapping(rename_skill, "--rename-skill")
78
+ apply_skill_feedback(skill_map_path, target=old, action="rename", replacement=new, note=feedback_note)
79
+ console.print(f"[green]✓[/] Renamed skill {old} -> {new}")
80
+ return
81
+ if merge_skill:
82
+ old, new = _split_mapping(merge_skill, "--merge-skill")
83
+ apply_skill_feedback(skill_map_path, target=old, action="merge", replacement=new, note=feedback_note)
84
+ console.print(f"[green]✓[/] Merged skill {old} -> {new}")
85
+ return
86
+
87
+ since_date = _today_start_iso() if today and not since else None
88
+ inputs = collect_learning_inputs(
89
+ root,
90
+ since=since,
91
+ since_date=since_date,
92
+ max_changed_files=cfg.learning.max_changed_files,
93
+ max_diff_chars_per_file=cfg.learning.max_diff_chars_per_file,
94
+ )
95
+ report = build_learning_report(
96
+ inputs,
97
+ max_cards=cfg.learning.max_cards,
98
+ max_quiz_questions=cfg.learning.max_quiz_questions,
99
+ )
100
+ feedback_summary = load_feedback_summary(root / cfg.learning.feedback_output)
101
+ report = apply_feedback_to_report(report, feedback_summary)
102
+ report.agent_lessons = rank_agent_lessons(report, feedback_summary, limit=cfg.learning.max_cards)
103
+ if today:
104
+ report.scope = "today"
105
+ if since_date:
106
+ report.since = f"today ({since_date})"
107
+
108
+ if provider_preview:
109
+ typer.echo(render_provider_preview_markdown(report), nl=False)
110
+ return
111
+
112
+ command = provider_command or cfg.learning.provider_command
113
+ if command:
114
+ try:
115
+ report = run_provider_command(command, report, timeout_seconds=cfg.learning.provider_timeout_seconds)
116
+ except LearningProviderError as exc:
117
+ console.print(f"[red]Provider command failed:[/] {exc}")
118
+ raise typer.Exit(1) from exc
119
+
120
+ update_skill_map(skill_map_path, report.skill_evidence)
121
+ agent_lessons_path = root / cfg.learning.agent_lessons_output
122
+ agent_lessons_path.parent.mkdir(parents=True, exist_ok=True)
123
+ _atomic_write(agent_lessons_path, render_agent_lessons_markdown(report))
124
+
125
+ quality = score_learning_report(report)
126
+ if quality.score < cfg.learning.min_groundedness_score:
127
+ console.print(f"[yellow]Learning quality warning:[/] score {quality.score}; " + "; ".join(quality.issues))
128
+
129
+ if llm_prompt:
130
+ prompt_path = root / cfg.learning.llm_prompt_output
131
+ prompt_path.parent.mkdir(parents=True, exist_ok=True)
132
+ _atomic_write(prompt_path, render_llm_prompt_markdown(report))
133
+ if pr_comment:
134
+ pr_path = root / cfg.learning.pr_comment_output
135
+ pr_path.parent.mkdir(parents=True, exist_ok=True)
136
+ _atomic_write(pr_path, render_pr_comment_markdown(report))
137
+ if dashboard:
138
+ dashboard_path = root / cfg.learning.dashboard_output
139
+ dashboard_path.parent.mkdir(parents=True, exist_ok=True)
140
+ _atomic_write(dashboard_path, render_dashboard_html(report))
141
+ if team_export:
142
+ team_path = root / cfg.learning.team_lessons_output
143
+ team_path.parent.mkdir(parents=True, exist_ok=True)
144
+ _atomic_write(team_path, render_team_lessons_markdown(report))
145
+ if feedback:
146
+ if feedback not in {"helpful", "not-helpful"}:
147
+ console.print("[red]--feedback must be helpful or not-helpful.[/]")
148
+ raise typer.Exit(2)
149
+ record_learning_feedback(root / cfg.learning.feedback_output, report, feedback, feedback_note, feedback_target)
150
+ if ci:
151
+ typer.echo(render_quality_markdown(report, quality.score, quality.issues), nl=False)
152
+ if quality.score < cfg.learning.min_groundedness_score:
153
+ raise typer.Exit(1)
154
+
155
+ if json_output:
156
+ typer.echo(json.dumps(learning_report_to_dict(report), indent=2, sort_keys=True))
157
+ return
158
+
159
+ default_output = cfg.learning.daily_output if today else cfg.learning.markdown_output
160
+ out_path = root / (output or default_output)
161
+ out_path.parent.mkdir(parents=True, exist_ok=True)
162
+ _atomic_write(out_path, render_learning_markdown(report))
163
+ console.print(f"[green]✓[/] Wrote {out_path.relative_to(root)}")
164
+
165
+
166
+ def _today_start_iso() -> str:
167
+ now = datetime.now().astimezone()
168
+ return now.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
169
+
170
+
171
+ def _split_mapping(value: str, flag: str) -> tuple[str, str]:
172
+ if "=>" not in value:
173
+ console.print(f"[red]{flag} expects old=>new.[/]")
174
+ raise typer.Exit(2)
175
+ old, new = [part.strip() for part in value.split("=>", 1)]
176
+ if not old or not new:
177
+ console.print(f"[red]{flag} expects non-empty old=>new values.[/]")
178
+ raise typer.Exit(2)
179
+ return old, new
@@ -23,6 +23,7 @@ class StageResult:
23
23
  duration_s: float
24
24
  returncode: int = 0
25
25
  detail: str = ""
26
+ output_excerpt: str = ""
26
27
 
27
28
  def as_dict(self) -> dict[str, Any]:
28
29
  return {
@@ -32,6 +33,7 @@ class StageResult:
32
33
  "duration_s": round(self.duration_s, 3),
33
34
  "returncode": self.returncode,
34
35
  "detail": self.detail,
36
+ "output_excerpt": self.output_excerpt,
35
37
  }
36
38
 
37
39
 
@@ -64,6 +66,8 @@ def register(app: typer.Typer) -> None:
64
66
  console.print(f"{marker} {stage.name}: {stage.status} ({stage.duration_s:.2f}s)")
65
67
  if stage.detail and stage.status != "passed":
66
68
  console.print(f" {stage.detail}")
69
+ if stage.output_excerpt and stage.status != "passed":
70
+ console.print(stage.output_excerpt)
67
71
  if stage.status != "passed":
68
72
  console.print(f" rerun: [bold]{stage.command}[/]")
69
73
  if failed:
@@ -93,7 +97,8 @@ def _run_stage(root: Path, name: str, command: list[str]) -> StageResult:
93
97
  result = subprocess.run(command, cwd=root, capture_output=True, text=True)
94
98
  except OSError as exc:
95
99
  return StageResult(name=name, command=" ".join(command), status="failed", duration_s=time.perf_counter() - started, returncode=1, detail=str(exc))
96
- output = (result.stderr or result.stdout).strip().splitlines()
100
+ combined_output = (result.stdout + "\n" + result.stderr).strip()
101
+ output = combined_output.splitlines()
97
102
  return StageResult(
98
103
  name=name,
99
104
  command=" ".join(command),
@@ -101,4 +106,14 @@ def _run_stage(root: Path, name: str, command: list[str]) -> StageResult:
101
106
  duration_s=time.perf_counter() - started,
102
107
  returncode=result.returncode,
103
108
  detail=output[-1] if output else "",
109
+ output_excerpt=_output_excerpt(combined_output) if result.returncode != 0 else "",
104
110
  )
111
+
112
+
113
+ def _output_excerpt(output: str, *, max_lines: int = 80) -> str:
114
+ lines = output.splitlines()
115
+ if len(lines) <= max_lines:
116
+ excerpt = lines
117
+ else:
118
+ excerpt = ["... output truncated to final failing lines ...", *lines[-max_lines:]]
119
+ return "\n".join(f" {line}" for line in excerpt)
@@ -55,6 +55,10 @@ class LearningConfig(BaseModel):
55
55
  llm_prompt_output: str = ".agentpack/learning.prompt.md"
56
56
  pr_comment_output: str = ".agentpack/pr-learning-comment.md"
57
57
  feedback_output: str = ".agentpack/learning-feedback.jsonl"
58
+ dashboard_output: str = ".agentpack/learning-dashboard.html"
59
+ team_lessons_output: str = ".agentpack/team-lessons.md"
60
+ provider_command: str = ""
61
+ provider_timeout_seconds: int = 60
58
62
  inject_agent_lessons: bool = True
59
63
  max_changed_files: int = 20
60
64
  max_diff_chars_per_file: int = 1200
@@ -184,6 +188,10 @@ agent_lessons_output = ".agentpack/agent-lessons.md"
184
188
  llm_prompt_output = ".agentpack/learning.prompt.md"
185
189
  pr_comment_output = ".agentpack/pr-learning-comment.md"
186
190
  feedback_output = ".agentpack/learning-feedback.jsonl"
191
+ dashboard_output = ".agentpack/learning-dashboard.html"
192
+ team_lessons_output = ".agentpack/team-lessons.md"
193
+ provider_command = ""
194
+ provider_timeout_seconds = 60
187
195
  inject_agent_lessons = true
188
196
  max_changed_files = 20
189
197
  max_diff_chars_per_file = 1200
@@ -1,5 +1,7 @@
1
1
  from agentpack.learning.models import (
2
2
  AgentLesson,
3
+ FeedbackSignal,
4
+ FeedbackSummary,
3
5
  LearningCard,
4
6
  LearningOptions,
5
7
  LearningReport,
@@ -11,6 +13,8 @@ from agentpack.learning.models import (
11
13
 
12
14
  __all__ = [
13
15
  "AgentLesson",
16
+ "FeedbackSignal",
17
+ "FeedbackSummary",
14
18
  "LearningCard",
15
19
  "LearningOptions",
16
20
  "LearningReport",
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+
7
+ from pydantic import ValidationError
8
+
9
+ from agentpack.learning.models import FeedbackSignal, FeedbackSummary, LearningReport
10
+
11
+
12
+ def record_learning_feedback(
13
+ path: Path,
14
+ report: LearningReport,
15
+ feedback: str,
16
+ note: str = "",
17
+ target: str = "",
18
+ ) -> None:
19
+ path.parent.mkdir(parents=True, exist_ok=True)
20
+ payload = FeedbackSignal(
21
+ created_at=datetime.now(timezone.utc).isoformat(),
22
+ task=report.task,
23
+ scope=report.scope,
24
+ feedback=feedback,
25
+ note=note,
26
+ target=target,
27
+ concepts=report.concepts,
28
+ source_files=[source.path for source in report.source_files],
29
+ ).model_dump(mode="json")
30
+ with path.open("a", encoding="utf-8") as fh:
31
+ fh.write(json.dumps(payload, sort_keys=True) + "\n")
32
+
33
+
34
+ def load_feedback_summary(path: Path) -> FeedbackSummary:
35
+ summary = FeedbackSummary()
36
+ if not path.exists():
37
+ return summary
38
+ for line in path.read_text(encoding="utf-8").splitlines():
39
+ if not line.strip():
40
+ continue
41
+ try:
42
+ signal = FeedbackSignal.model_validate_json(line)
43
+ except (ValidationError, ValueError):
44
+ continue
45
+ _apply_signal(summary, signal)
46
+ return summary
47
+
48
+
49
+ def apply_feedback_to_report(report: LearningReport, summary: FeedbackSummary) -> LearningReport:
50
+ if not _has_feedback(summary):
51
+ return report
52
+
53
+ rename_map = {old.lower(): new for old, new in summary.skill_renames.items()}
54
+ merge_map = {old.lower(): new for old, new in summary.skill_merges.items()}
55
+ suppressed_skills = {skill.lower() for skill in summary.suppressed_skills}
56
+ not_helpful = {concept.lower() for concept in summary.not_helpful_concepts}
57
+ suppressed = suppressed_skills | not_helpful
58
+
59
+ for source in report.source_files:
60
+ source.concepts = [_normalize_concept(concept, rename_map, merge_map) for concept in source.concepts]
61
+ source.concepts = [concept for concept in source.concepts if concept.lower() not in suppressed]
62
+
63
+ report.concepts = [_normalize_concept(concept, rename_map, merge_map) for concept in report.concepts]
64
+ report.concepts = _dedupe([concept for concept in report.concepts if concept.lower() not in suppressed])
65
+
66
+ report.learning_cards = [
67
+ card
68
+ for card in report.learning_cards
69
+ if card.title.lower() not in suppressed
70
+ and not any(concept.lower() == card.title.lower() for concept in summary.not_helpful_concepts)
71
+ ]
72
+ for card in report.learning_cards:
73
+ normalized = _normalize_concept(card.title, rename_map, merge_map)
74
+ if normalized != card.title:
75
+ card.title = normalized
76
+ if card.title.lower() in {item.lower() for item in summary.helpful_concepts}:
77
+ card.body = f"{card.body} Prior feedback marked this concept useful; keep practicing it with changed-file evidence."
78
+
79
+ report.agent_lessons = [
80
+ lesson
81
+ for lesson in report.agent_lessons
82
+ if not _lesson_suppressed(lesson.rule, summary)
83
+ ]
84
+ for lesson in report.agent_lessons:
85
+ if _lesson_helpful(lesson.rule, report.concepts, summary):
86
+ lesson.status = "accepted"
87
+ lesson.confidence = min(100, max(lesson.confidence, 90))
88
+
89
+ for item in report.skill_evidence:
90
+ item.skill = _normalize_concept(item.skill, rename_map, merge_map)
91
+ if item.skill.lower() in {concept.lower() for concept in summary.helpful_concepts}:
92
+ item.confidence = min(100, item.confidence + 10)
93
+ report.skill_evidence = [
94
+ item for item in report.skill_evidence if item.skill.lower() not in suppressed
95
+ ]
96
+ return report
97
+
98
+
99
+ def _apply_signal(summary: FeedbackSummary, signal: FeedbackSignal) -> None:
100
+ target = signal.target.strip()
101
+ target_kind, _, target_value = target.partition(":")
102
+ normalized_feedback = signal.feedback.strip().lower()
103
+ if normalized_feedback == "helpful":
104
+ summary.helpful_concepts.update(signal.concepts)
105
+ if signal.note:
106
+ summary.accepted_notes.append(signal.note)
107
+ elif normalized_feedback == "not-helpful":
108
+ summary.not_helpful_concepts.update(signal.concepts)
109
+
110
+ if target_kind == "skill" and target_value:
111
+ if normalized_feedback == "not-helpful":
112
+ summary.suppressed_skills.add(target_value)
113
+ elif normalized_feedback == "helpful":
114
+ summary.helpful_concepts.add(target_value)
115
+ elif target_kind == "lesson" and target_value and normalized_feedback == "not-helpful":
116
+ summary.suppressed_lesson_terms.add(target_value)
117
+ elif target_kind == "rename" and "=>" in target_value:
118
+ old, new = [part.strip() for part in target_value.split("=>", 1)]
119
+ if old and new:
120
+ summary.skill_renames[old] = new
121
+ elif target_kind == "merge" and "=>" in target_value:
122
+ old, new = [part.strip() for part in target_value.split("=>", 1)]
123
+ if old and new:
124
+ summary.skill_merges[old] = new
125
+
126
+
127
+ def _has_feedback(summary: FeedbackSummary) -> bool:
128
+ return bool(
129
+ summary.helpful_concepts
130
+ or summary.not_helpful_concepts
131
+ or summary.suppressed_skills
132
+ or summary.suppressed_lesson_terms
133
+ or summary.skill_renames
134
+ or summary.skill_merges
135
+ )
136
+
137
+
138
+ def _normalize_concept(concept: str, renames: dict[str, str], merges: dict[str, str]) -> str:
139
+ key = concept.lower()
140
+ return merges.get(key) or renames.get(key) or concept
141
+
142
+
143
+ def _lesson_suppressed(rule: str, summary: FeedbackSummary) -> bool:
144
+ lower = rule.lower()
145
+ return any(term.lower() in lower for term in summary.suppressed_lesson_terms)
146
+
147
+
148
+ def _lesson_helpful(rule: str, concepts: list[str], summary: FeedbackSummary) -> bool:
149
+ lower_rule = rule.lower()
150
+ helpful = {concept.lower() for concept in summary.helpful_concepts}
151
+ for concept in concepts:
152
+ lower_concept = concept.lower()
153
+ if lower_concept not in helpful:
154
+ continue
155
+ if lower_concept in lower_rule:
156
+ return True
157
+ if any(part and part in lower_rule for part in lower_concept.split()):
158
+ return True
159
+ return False
160
+
161
+
162
+ def _dedupe(values: list[str]) -> list[str]:
163
+ seen: set[str] = set()
164
+ result: list[str] = []
165
+ for value in values:
166
+ key = value.lower()
167
+ if key in seen:
168
+ continue
169
+ seen.add(key)
170
+ result.append(value)
171
+ return result