agentpack-cli 0.3.16__tar.gz → 0.3.17__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 (142) hide show
  1. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/PKG-INFO +4 -1
  2. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/README.md +2 -0
  3. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/pyproject.toml +3 -1
  4. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/__init__.py +1 -1
  5. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/cli.py +2 -0
  6. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/benchmark.py +10 -10
  7. agentpack_cli-0.3.17/src/agentpack/commands/dashboard.py +43 -0
  8. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/eval_cmd.py +7 -7
  9. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/explain.py +3 -3
  10. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/init.py +8 -0
  11. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/next_cmd.py +36 -0
  12. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/scan.py +1 -1
  13. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/skills.py +7 -5
  14. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/tune.py +1 -1
  15. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/workflow_cmd.py +110 -5
  16. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/config.py +30 -0
  17. agentpack_cli-0.3.17/src/agentpack/core/loop_protocol.py +349 -0
  18. agentpack_cli-0.3.17/src/agentpack/dashboard/__init__.py +1 -0
  19. agentpack_cli-0.3.17/src/agentpack/dashboard/collectors.py +520 -0
  20. agentpack_cli-0.3.17/src/agentpack/dashboard/models.py +156 -0
  21. agentpack_cli-0.3.17/src/agentpack/dashboard/renderers.py +275 -0
  22. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/router/discovery.py +16 -31
  23. agentpack_cli-0.3.17/src/agentpack/router/skills_index.py +191 -0
  24. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/.gitignore +0 -0
  25. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/LICENSE +0 -0
  26. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/adapters/__init__.py +0 -0
  27. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/adapters/antigravity.py +0 -0
  28. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/adapters/base.py +0 -0
  29. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/adapters/claude.py +0 -0
  30. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/adapters/codex.py +0 -0
  31. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/adapters/cursor.py +0 -0
  32. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/adapters/detect.py +0 -0
  33. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/adapters/generic.py +0 -0
  34. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/adapters/windsurf.py +0 -0
  35. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/__init__.py +0 -0
  36. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/dependency_graph.py +0 -0
  37. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/go_imports.py +0 -0
  38. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/java_imports.py +0 -0
  39. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/js_ts_imports.py +0 -0
  40. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/monorepo.py +0 -0
  41. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/naming_signals.py +0 -0
  42. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/python_imports.py +0 -0
  43. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/ranking.py +0 -0
  44. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/repo_map.py +0 -0
  45. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/role_inference.py +0 -0
  46. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/rust_imports.py +0 -0
  47. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/symbols.py +0 -0
  48. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/task_classifier.py +0 -0
  49. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/analysis/tests.py +0 -0
  50. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/application/__init__.py +0 -0
  51. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/application/pack_service.py +0 -0
  52. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/__init__.py +0 -0
  53. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/_shared.py +0 -0
  54. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/ci_cmd.py +0 -0
  55. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/claude_cmd.py +0 -0
  56. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/dev_check.py +0 -0
  57. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/diagnose_selection.py +0 -0
  58. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/diff.py +0 -0
  59. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/doctor.py +0 -0
  60. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/guard.py +0 -0
  61. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/hook_cmd.py +0 -0
  62. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/ignore_cmd.py +0 -0
  63. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/install.py +0 -0
  64. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/learn.py +0 -0
  65. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/mcp_cmd.py +0 -0
  66. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/migrate.py +0 -0
  67. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/monitor.py +0 -0
  68. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/pack.py +0 -0
  69. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/quickstart.py +0 -0
  70. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/release_check.py +0 -0
  71. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/release_cmd.py +0 -0
  72. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/repair.py +0 -0
  73. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/route.py +0 -0
  74. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/start_cmd.py +0 -0
  75. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/state_cmd.py +0 -0
  76. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/stats.py +0 -0
  77. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/status.py +0 -0
  78. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/summarize.py +0 -0
  79. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/task_cmd.py +0 -0
  80. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/threads.py +0 -0
  81. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/verify_wheel.py +0 -0
  82. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/commands/watch.py +0 -0
  83. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/__init__.py +0 -0
  84. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/bootstrap.py +0 -0
  85. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/cache.py +0 -0
  86. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/changed_paths.py +0 -0
  87. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/context_pack.py +0 -0
  88. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/diff.py +0 -0
  89. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/evals.py +0 -0
  90. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/execution_state.py +0 -0
  91. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/git.py +0 -0
  92. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/git_hooks.py +0 -0
  93. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/global_install.py +0 -0
  94. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/ignore.py +0 -0
  95. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/merkle.py +0 -0
  96. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/models.py +0 -0
  97. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/redactor.py +0 -0
  98. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/scanner.py +0 -0
  99. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/snapshot.py +0 -0
  100. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/task_freshness.py +0 -0
  101. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/thread_context.py +0 -0
  102. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/token_estimator.py +0 -0
  103. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/core/vscode_tasks.py +0 -0
  104. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/data/agentpack.md +0 -0
  105. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/installers/__init__.py +0 -0
  106. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/installers/antigravity.py +0 -0
  107. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/installers/claude.py +0 -0
  108. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/installers/codex.py +0 -0
  109. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/installers/cursor.py +0 -0
  110. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/installers/windsurf.py +0 -0
  111. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/integrations/__init__.py +0 -0
  112. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/integrations/agents.py +0 -0
  113. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/integrations/git_hooks.py +0 -0
  114. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/integrations/global_install.py +0 -0
  115. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/integrations/platform.py +0 -0
  116. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/integrations/vscode_tasks.py +0 -0
  117. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/learning/__init__.py +0 -0
  118. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/learning/collector.py +0 -0
  119. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/learning/extractor.py +0 -0
  120. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/learning/feedback.py +0 -0
  121. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/learning/lesson_ranker.py +0 -0
  122. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/learning/models.py +0 -0
  123. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/learning/provider.py +0 -0
  124. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/learning/quality.py +0 -0
  125. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/learning/renderers.py +0 -0
  126. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/learning/skill_map.py +0 -0
  127. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/mcp_server.py +0 -0
  128. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/renderers/__init__.py +0 -0
  129. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/renderers/compact.py +0 -0
  130. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/renderers/markdown.py +0 -0
  131. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/renderers/receipts.py +0 -0
  132. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/router/__init__.py +0 -0
  133. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/router/models.py +0 -0
  134. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/router/parser.py +0 -0
  135. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/router/prompt_builder.py +0 -0
  136. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/router/scoring.py +0 -0
  137. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/router/service.py +0 -0
  138. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/session/__init__.py +0 -0
  139. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/session/state.py +0 -0
  140. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/summaries/__init__.py +0 -0
  141. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/src/agentpack/summaries/base.py +0 -0
  142. {agentpack_cli-0.3.16 → agentpack_cli-0.3.17}/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.16
3
+ Version: 0.3.17
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
@@ -29,6 +29,7 @@ Requires-Dist: watchdog>=4.0.0; extra == 'all'
29
29
  Provides-Extra: dev
30
30
  Requires-Dist: mypy; extra == 'dev'
31
31
  Requires-Dist: pytest; extra == 'dev'
32
+ Requires-Dist: pytest-asyncio; extra == 'dev'
32
33
  Requires-Dist: pytest-cov; extra == 'dev'
33
34
  Requires-Dist: ruff; extra == 'dev'
34
35
  Requires-Dist: tomli>=2.0.0; (python_version < '3.11') and extra == 'dev'
@@ -376,6 +377,7 @@ gate.
376
377
  | `agentpack guard --repair-stale --refresh-context` | Check freshness, repair stale rules, refresh context |
377
378
  | `agentpack status` | Show context freshness and git/task state |
378
379
  | `agentpack stats` | Show pack size, token savings, and top files |
380
+ | `agentpack dashboard` | Local HTML control plane for context, skills, learning, and benchmark quality |
379
381
  | `agentpack explain --task auto` | Debug selected and omitted files |
380
382
  | `agentpack diagnose-selection` | Turn latest pack/benchmark signals into concrete tuning actions |
381
383
  | `agentpack ignore suggest|apply` | Suggest or apply `.agentignore` improvements |
@@ -459,6 +461,7 @@ AgentPack writes local artifacts under `.agentpack/`:
459
461
  | `.agentpack/learning.prompt.md` | optional source-backed prompt for external LLM refinement |
460
462
  | `.agentpack/pr-learning-comment.md` | optional PR-comment-ready learning summary |
461
463
  | `.agentpack/learning-dashboard.html` | optional static dashboard from `agentpack learn --dashboard` |
464
+ | `.agentpack/dashboard.html` | local project dashboard from `agentpack dashboard` |
462
465
  | `.agentpack/team-lessons.md` | optional shared lesson export from `agentpack learn --team-export` |
463
466
  | `.agentpack/learning-feedback.jsonl` | optional local helpful/not-helpful feedback records |
464
467
  | `.agentpack/pack_metadata.json` | freshness and pack metadata |
@@ -336,6 +336,7 @@ gate.
336
336
  | `agentpack guard --repair-stale --refresh-context` | Check freshness, repair stale rules, refresh context |
337
337
  | `agentpack status` | Show context freshness and git/task state |
338
338
  | `agentpack stats` | Show pack size, token savings, and top files |
339
+ | `agentpack dashboard` | Local HTML control plane for context, skills, learning, and benchmark quality |
339
340
  | `agentpack explain --task auto` | Debug selected and omitted files |
340
341
  | `agentpack diagnose-selection` | Turn latest pack/benchmark signals into concrete tuning actions |
341
342
  | `agentpack ignore suggest|apply` | Suggest or apply `.agentignore` improvements |
@@ -419,6 +420,7 @@ AgentPack writes local artifacts under `.agentpack/`:
419
420
  | `.agentpack/learning.prompt.md` | optional source-backed prompt for external LLM refinement |
420
421
  | `.agentpack/pr-learning-comment.md` | optional PR-comment-ready learning summary |
421
422
  | `.agentpack/learning-dashboard.html` | optional static dashboard from `agentpack learn --dashboard` |
423
+ | `.agentpack/dashboard.html` | local project dashboard from `agentpack dashboard` |
422
424
  | `.agentpack/team-lessons.md` | optional shared lesson export from `agentpack learn --team-export` |
423
425
  | `.agentpack/learning-feedback.jsonl` | optional local helpful/not-helpful feedback records |
424
426
  | `.agentpack/pack_metadata.json` | freshness and pack metadata |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentpack-cli"
3
- version = "0.3.16"
3
+ version = "0.3.17"
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"
@@ -47,6 +47,7 @@ all = [
47
47
 
48
48
  dev = [
49
49
  "pytest",
50
+ "pytest-asyncio",
50
51
  "pytest-cov",
51
52
  "ruff",
52
53
  "mypy",
@@ -55,6 +56,7 @@ dev = [
55
56
 
56
57
  [tool.pytest.ini_options]
57
58
  pythonpath = ["src"]
59
+ asyncio_default_fixture_loop_scope = "function"
58
60
  markers = [
59
61
  "slow: marks tests as slow (deselect with '-m \"not slow\"')",
60
62
  ]
@@ -1,3 +1,3 @@
1
1
  """AgentPack — task-aware context packing for AI coding agents."""
2
2
 
3
- __version__ = "0.3.16"
3
+ __version__ = "0.3.17"
@@ -5,6 +5,7 @@ from agentpack.commands import (
5
5
  benchmark,
6
6
  claude_cmd,
7
7
  ci_cmd,
8
+ dashboard,
8
9
  dev_check,
9
10
  diagnose_selection,
10
11
  diff,
@@ -70,6 +71,7 @@ for mod in [
70
71
  task_cmd,
71
72
  threads,
72
73
  stats,
74
+ dashboard,
73
75
  summarize,
74
76
  learn,
75
77
  pack,
@@ -1930,21 +1930,21 @@ def benchmark(
1930
1930
  mode: str = typer.Option("balanced", "--mode", help="Mode for single-task run (lite|minimal|balanced|deep)."),
1931
1931
  workspace: str = typer.Option("", "--workspace", help="Restrict benchmark packs to a workspace, e.g. apps/web."),
1932
1932
  cases: str = typer.Option("", "--cases", help="Path to TOML cases file (default: .agentpack/benchmark.toml)."),
1933
- compare: bool = typer.Option(False, "--compare", is_flag=True, help="Compare minimal/balanced/deep for each task."),
1934
- init: bool = typer.Option(False, "--init", is_flag=True, help="Scaffold a benchmark.toml and exit."),
1935
- results_template: bool = typer.Option(False, "--results-template", is_flag=True, help="Create benchmarks/results/YYYY-MM-DD.md for publishing benchmark evidence."),
1933
+ compare: bool = typer.Option(False, "--compare", help="Compare minimal/balanced/deep for each task."),
1934
+ init: bool = typer.Option(False, "--init", help="Scaffold a benchmark.toml and exit."),
1935
+ results_template: bool = typer.Option(False, "--results-template", help="Create benchmarks/results/YYYY-MM-DD.md for publishing benchmark evidence."),
1936
1936
  from_history: int = typer.Option(0, "--from-history", help="Sample last N unique tasks from metrics.jsonl history."),
1937
1937
  write_cases: bool = typer.Option(False, "--write-cases", help="Append --from-history cases to .agentpack/benchmark.toml."),
1938
- sample_fixtures: bool = typer.Option(False, "--sample-fixtures", is_flag=True, help="Run bundled FastAPI/Next.js/mixed-repo fixture evals from a source checkout."),
1939
- release_gate: bool = typer.Option(False, "--release-gate", is_flag=True, help="Run the public real-repo release gate."),
1940
- public_repos: bool = typer.Option(False, "--public-repos", is_flag=True, help="Run real public-repo commit cases from benchmarks/public-repos.toml."),
1938
+ sample_fixtures: bool = typer.Option(False, "--sample-fixtures", help="Run bundled FastAPI/Next.js/mixed-repo fixture evals from a source checkout."),
1939
+ release_gate: bool = typer.Option(False, "--release-gate", help="Run the public real-repo release gate."),
1940
+ public_repos: bool = typer.Option(False, "--public-repos", help="Run real public-repo commit cases from benchmarks/public-repos.toml."),
1941
1941
  public_repos_file: str = typer.Option("", "--public-repos-file", help="Path to public repo benchmark manifest."),
1942
1942
  public_repos_cache: str = typer.Option("", "--public-repos-cache", help="Directory for cached public repo clones."),
1943
- refresh_public_repos: bool = typer.Option(False, "--refresh-public-repos", is_flag=True, help="Delete and reclone public repo benchmark cache before running."),
1944
- public_table: bool = typer.Option(False, "--public-table", is_flag=True, help="Write a publishable Markdown benchmark table under benchmarks/results/."),
1943
+ refresh_public_repos: bool = typer.Option(False, "--refresh-public-repos", help="Delete and reclone public repo benchmark cache before running."),
1944
+ public_table: bool = typer.Option(False, "--public-table", help="Write a publishable Markdown benchmark table under benchmarks/results/."),
1945
1945
  no_public_table: bool = typer.Option(False, "--no-public-table", help="Do not write a benchmark results markdown table."),
1946
- misses: bool = typer.Option(False, "--misses", is_flag=True, help="Show diagnostics for expected files that were not selected."),
1947
- prove_targets: bool = typer.Option(False, "--prove-targets", is_flag=True, help="Exit non-zero unless recall/token precision targets pass."),
1946
+ misses: bool = typer.Option(False, "--misses", help="Show diagnostics for expected files that were not selected."),
1947
+ prove_targets: bool = typer.Option(False, "--prove-targets", help="Exit non-zero unless recall/token precision targets pass."),
1948
1948
  min_recall: float = typer.Option(0.60, "--min-recall", help="Recall target for --prove-targets."),
1949
1949
  min_token_precision: float = typer.Option(0.50, "--min-token-precision", help="Token precision target for --prove-targets."),
1950
1950
  ) -> None:
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import typer
9
+
10
+ from agentpack.commands._shared import _atomic_write, _root, console
11
+ from agentpack.dashboard.collectors import build_project_dashboard_snapshot
12
+ from agentpack.dashboard.renderers import render_dashboard_html
13
+
14
+
15
+ def register(app: typer.Typer) -> None:
16
+ @app.command()
17
+ def dashboard(
18
+ json_output: bool = typer.Option(False, "--json", help="Print normalized dashboard snapshot JSON."),
19
+ open_browser: bool = typer.Option(False, "--open", help="Open the generated HTML dashboard."),
20
+ output: str = typer.Option("", "--output", "-o", help="Dashboard HTML output path."),
21
+ ) -> None:
22
+ """Generate a local AgentPack dashboard."""
23
+ root = _root()
24
+ snapshot = build_project_dashboard_snapshot(root)
25
+ if json_output:
26
+ typer.echo(json.dumps(snapshot.model_dump(mode="json"), indent=2, sort_keys=True))
27
+ return
28
+
29
+ out = root / (output or ".agentpack/dashboard.html")
30
+ out.parent.mkdir(parents=True, exist_ok=True)
31
+ _atomic_write(out, render_dashboard_html(snapshot))
32
+ console.print(f"[green]✓[/] Wrote [bold]{out}[/]")
33
+ if open_browser:
34
+ _open_file(out)
35
+
36
+
37
+ def _open_file(path: Path) -> None:
38
+ if sys.platform == "darwin":
39
+ subprocess.run(["open", str(path)], check=False)
40
+ elif sys.platform.startswith("win"):
41
+ subprocess.run(["cmd", "/c", "start", "", str(path)], check=False)
42
+ else:
43
+ subprocess.run(["xdg-open", str(path)], check=False)
@@ -28,25 +28,25 @@ from agentpack.core.evals import (
28
28
  def register(app: typer.Typer) -> None:
29
29
  @app.command(name="eval")
30
30
  def eval_command(
31
- init: bool = typer.Option(False, "--init", is_flag=True, help="Scaffold .agentpack/evals.toml and exit."),
31
+ init: bool = typer.Option(False, "--init", help="Scaffold .agentpack/evals.toml and exit."),
32
32
  cases: str = typer.Option("", "--cases", help="Path to eval TOML file (default: .agentpack/evals.toml)."),
33
33
  case: str = typer.Option("", "--case", help="Run one eval case by id."),
34
- prove_targets: bool = typer.Option(False, "--prove-targets", is_flag=True, help="Exit non-zero when any eval case fails."),
34
+ prove_targets: bool = typer.Option(False, "--prove-targets", help="Exit non-zero when any eval case fails."),
35
35
  capture: str = typer.Option("", "--capture", help="Append a case from current git diff using this id."),
36
36
  failure_class: str = typer.Option("context", "--failure-class", help=f"Failure class ({' | '.join(FAILURE_CLASSES)})."),
37
37
  failure_source: str = typer.Option("agent_failed", "--failure-source", help="Failure source for captured cases."),
38
38
  check: list[str] | None = typer.Option(None, "--check", help="Deterministic command check for --capture. Repeatable."),
39
39
  task: str = typer.Option("", "--task", help="Task text for --capture."),
40
40
  base_ref: str = typer.Option("HEAD", "--base-ref", help="Git base ref for diff checks."),
41
- report: bool = typer.Option(False, "--report", is_flag=True, help="Write benchmarks/results/YYYY-MM-DD-eval.md."),
42
- ci_template: bool = typer.Option(False, "--ci-template", is_flag=True, help="Scaffold .github/workflows/agentpack-eval.yml and exit."),
41
+ report: bool = typer.Option(False, "--report", help="Write benchmarks/results/YYYY-MM-DD-eval.md."),
42
+ ci_template: bool = typer.Option(False, "--ci-template", help="Scaffold .github/workflows/agentpack-eval.yml and exit."),
43
43
  variant: str = typer.Option("agentpack", "--variant", help="Result variant label, e.g. baseline or agentpack."),
44
44
  compare_variants: str = typer.Option("", "--compare-variants", help="Compare latest results as BASELINE:VARIANT."),
45
- replay: bool = typer.Option(False, "--replay", is_flag=True, help="Run cases in isolated git worktrees using captured patch_file artifacts."),
46
- watch: bool = typer.Option(False, "--watch", is_flag=True, help="Rerun evals when git diff state changes."),
45
+ replay: bool = typer.Option(False, "--replay", help="Run cases in isolated git worktrees using captured patch_file artifacts."),
46
+ watch: bool = typer.Option(False, "--watch", help="Rerun evals when git diff state changes."),
47
47
  interval: float = typer.Option(2.0, "--interval", help="Watch polling interval in seconds."),
48
48
  max_runs: int = typer.Option(0, "--max-runs", help="Maximum watch runs (0 = unlimited)."),
49
- until_pass: bool = typer.Option(False, "--until-pass", is_flag=True, help="Stop watch mode after all cases pass."),
49
+ until_pass: bool = typer.Option(False, "--until-pass", help="Stop watch mode after all cases pass."),
50
50
  agent: str = typer.Option("", "--agent", help="Agent label to store with --capture metadata."),
51
51
  prompt_file: str = typer.Option("", "--prompt-file", help="Prompt artifact path to store with --capture."),
52
52
  context_file: str = typer.Option(".agentpack/context.md", "--context-file", help="Context artifact path to store with --capture."),
@@ -294,9 +294,9 @@ def register(app: typer.Typer) -> None:
294
294
  budget: int = typer.Option(0, "--budget", help="Token budget (0 = use config default)."),
295
295
  since: Optional[str] = typer.Option(None, "--since", help="Git ref to compare against (e.g. HEAD~1, main)."),
296
296
  file: Optional[str] = typer.Option(None, "--file", help="Show detailed score breakdown for a specific file."),
297
- omitted: bool = typer.Option(False, "--omitted", is_flag=True, help="Show top-10 excluded files and why."),
298
- why_noisy: bool = typer.Option(False, "--why-noisy", is_flag=True, help="Explain broad task terms and noisy selection signals."),
299
- budget_plan: bool = typer.Option(False, "--budget-plan", is_flag=True, help="Show selected modes, token costs, and value per token."),
297
+ omitted: bool = typer.Option(False, "--omitted", help="Show top-10 excluded files and why."),
298
+ why_noisy: bool = typer.Option(False, "--why-noisy", help="Explain broad task terms and noisy selection signals."),
299
+ budget_plan: bool = typer.Option(False, "--budget-plan", help="Show selected modes, token costs, and value per token."),
300
300
  ) -> None:
301
301
  """Explain which files would be selected and why, without writing a context file."""
302
302
  if mode not in ("lite", "minimal", "balanced", "deep"):
@@ -71,6 +71,10 @@ def _repo_gitignore_entries(share_cache: bool = False, agent: str = "generic") -
71
71
  ".agentpack/learning-dashboard.html",
72
72
  ".agentpack/team-lessons.md",
73
73
  ".agentpack/learning-feedback.jsonl",
74
+ ".agentpack/loop_state.json",
75
+ ".agentpack/progress.md",
76
+ ".agentpack/loop_events.jsonl",
77
+ ".agentpack/loop_failures.jsonl",
74
78
  ".agentignore",
75
79
  ]
76
80
  )
@@ -112,6 +116,10 @@ def _agentpack_gitignore_content(share_cache: bool = False) -> str:
112
116
  "learning-dashboard.html",
113
117
  "team-lessons.md",
114
118
  "learning-feedback.jsonl",
119
+ "loop_state.json",
120
+ "progress.md",
121
+ "loop_events.jsonl",
122
+ "loop_failures.jsonl",
115
123
  ]
116
124
  )
117
125
  return "\n".join(entries) + "\n"
@@ -7,9 +7,12 @@ import typer
7
7
  from agentpack.commands._shared import console, _root, run_refresh
8
8
  from agentpack.commands.diagnose_selection import build_selection_diagnosis, _markdown_report
9
9
  from agentpack.commands.guard import _context_is_fresh
10
+ from agentpack.core.config import load_config
10
11
  from agentpack.core.context_pack import load_pack_metadata
12
+ from agentpack.core.loop_protocol import load_loop_state
11
13
  from agentpack.core.thread_context import detect_conflicts, list_thread_rows
12
14
  from agentpack.integrations.platform import cli_module_argv
15
+ from agentpack.router.skills_index import ensure_inventory_index
13
16
  from agentpack.session.state import TASK_FILE
14
17
  import subprocess
15
18
 
@@ -60,6 +63,8 @@ def _recommendations(root) -> list[dict[str, str]]:
60
63
  items.append({"kind": "thread_conflict", "command": "agentpack threads --conflicts", "reason": "active threads overlap on this branch/worktree"})
61
64
  if _pack_looks_noisy(root):
62
65
  items.append({"kind": "selection_noise", "command": "agentpack diagnose-selection", "reason": "latest pack has broad/noisy selection signals"})
66
+ items.extend(_skills_index_recommendations(root))
67
+ items.extend(_loop_recommendations(root))
63
68
  return items
64
69
 
65
70
 
@@ -90,6 +95,37 @@ def _pack_looks_noisy(root) -> bool:
90
95
  return False
91
96
 
92
97
 
98
+ def _skills_index_recommendations(root) -> list[dict[str, str]]:
99
+ cfg = load_config(root)
100
+ try:
101
+ ensure_inventory_index(root, cfg.skills.paths)
102
+ except Exception as exc:
103
+ return [
104
+ {
105
+ "kind": "skills_index_failed",
106
+ "command": "agentpack skills index",
107
+ "reason": f"automatic skills index refresh failed: {exc}",
108
+ }
109
+ ]
110
+ return []
111
+
112
+
113
+ def _loop_recommendations(root) -> list[dict[str, str]]:
114
+ cfg = load_config(root)
115
+ if not cfg.loop.enabled:
116
+ return []
117
+ state = load_loop_state(root)
118
+ if state is None:
119
+ return []
120
+ if not state.runner:
121
+ return [{"kind": "loop_runner_missing", "command": 'agentpack work "..." --run --runner "..."', "reason": "Ralph Loop state exists but no runner is configured"}]
122
+ if state.status == "ready_to_finish":
123
+ return [{"kind": "loop_ready_to_finish", "command": "agentpack finish --since main", "reason": "Ralph Loop verification passed"}]
124
+ if state.status == "blocked":
125
+ return [{"kind": "loop_blocked", "command": "agentpack dashboard", "reason": f"Ralph Loop blocked: {state.blocked_reason or 'inspect loop failures'}"}]
126
+ return [{"kind": "loop_continue", "command": f'agentpack work "{state.task}" --run', "reason": f"Ralph Loop is {state.status}"}]
127
+
128
+
93
129
  def _fix_all_safe(root, recommendations: list[dict[str, str]]) -> tuple[list[dict[str, str]], list[dict[str, str | int]]]:
94
130
  fixes: list[dict[str, str | int]] = []
95
131
  if any(item["kind"] == "init" for item in recommendations):
@@ -16,7 +16,7 @@ def register(app: typer.Typer) -> None:
16
16
  @app.command(name="scan")
17
17
  def scan_cmd(
18
18
  largest: int = typer.Option(10, "--largest", min=0, help="Show the N largest packable files by estimated tokens."),
19
- ignored_summary: bool = typer.Option(False, "--ignored-summary", is_flag=True, help="Group ignored/binary files by directory or extension."),
19
+ ignored_summary: bool = typer.Option(False, "--ignored-summary", help="Group ignored/binary files by directory or extension."),
20
20
  ) -> None:
21
21
  """Scan the repository and report file statistics."""
22
22
  root = _root()
@@ -9,7 +9,8 @@ from rich.table import Table
9
9
  from agentpack.commands._shared import _root, console
10
10
  from agentpack.core.config import load_config
11
11
  from agentpack.router.prompt_builder import render_plain
12
- from agentpack.router.discovery import discover_inventory, write_inventory_index
12
+ from agentpack.router.discovery import discover_inventory
13
+ from agentpack.router.skills_index import ensure_inventory_index
13
14
  from agentpack.router.service import RouteService
14
15
 
15
16
  skills_app = typer.Typer(help="Inspect and index local agent skills and rules.")
@@ -44,10 +45,10 @@ def index_skills() -> None:
44
45
  """Write .agentpack/skills_index.json."""
45
46
  root = _root()
46
47
  cfg = load_config(root)
47
- inventory = discover_inventory(root, cfg.skills.paths)
48
- path = write_inventory_index(root, inventory)
48
+ result = ensure_inventory_index(root, cfg.skills.paths, force=True)
49
+ inventory = result.document.inventory
49
50
  console.print(
50
- f"Indexed {len(inventory.skills)} skills and {len(inventory.rules)} rules at {path}"
51
+ f"Indexed {len(inventory.skills)} skills and {len(inventory.rules)} rules at {result.path}"
51
52
  )
52
53
 
53
54
 
@@ -102,5 +103,6 @@ def record_skill_feedback(
102
103
  "tests_passed": tests_passed,
103
104
  "user_feedback": user_feedback.strip(),
104
105
  }
105
- out.open("a", encoding="utf-8").write(json.dumps(record) + "\n")
106
+ with out.open("a", encoding="utf-8") as handle:
107
+ handle.write(json.dumps(record) + "\n")
106
108
  console.print(f"[green]✓[/] Recorded skill feedback in [bold]{out}[/]")
@@ -22,7 +22,7 @@ def register(app: typer.Typer) -> None:
22
22
  @app.command()
23
23
  def tune(
24
24
  from_benchmark: bool = typer.Option(True, "--from-benchmark/--no-benchmark", help="Use .agentpack/benchmark_results.jsonl."),
25
- write: bool = typer.Option(False, "--write", is_flag=True, help="Write suggestions to .agentpack/tuning.md."),
25
+ write: bool = typer.Option(False, "--write", help="Write suggestions to .agentpack/tuning.md."),
26
26
  ) -> None:
27
27
  """Suggest tuning actions from benchmark misses and recent pack metrics."""
28
28
  root = _root()
@@ -7,7 +7,18 @@ from typing import Any
7
7
 
8
8
  import typer
9
9
 
10
- from agentpack.commands._shared import console, _root
10
+ from agentpack.commands._shared import console, _root, run_refresh
11
+ from agentpack.commands.guard import _context_is_fresh
12
+ from agentpack.core.config import load_config
13
+ from agentpack.core.loop_protocol import (
14
+ LoopCommandResult,
15
+ dry_run_plan,
16
+ finish_blockers,
17
+ initialize_loop,
18
+ load_loop_state,
19
+ mark_done,
20
+ run_loop,
21
+ )
11
22
  from agentpack.core.thread_context import resolve_thread_option
12
23
  from agentpack.integrations.platform import cli_module_argv
13
24
 
@@ -24,6 +35,11 @@ def register(app: typer.Typer) -> None:
24
35
  pack_only: bool = typer.Option(False, "--pack-only", help="Run pack directly instead of guard."),
25
36
  no_init: bool = typer.Option(False, "--no-init", help="Do not initialize the repo when .agentpack/config.toml is missing."),
26
37
  no_next: bool = typer.Option(False, "--no-next", help="Do not print next-step diagnostics after context refresh."),
38
+ run_loop_requested: bool = typer.Option(False, "--run", help="Run the configured Ralph Loop after preparing context."),
39
+ dry_run: bool = typer.Option(False, "--dry-run", help="Plan Ralph Loop execution without running the configured runner."),
40
+ runner: str = typer.Option("", "--runner", help="Generic shell command for the Ralph Loop runner."),
41
+ max_iterations: int = typer.Option(0, "--max-iterations", help="Override [loop].max_iterations for this run."),
42
+ verify: list[str] = typer.Option([], "--verify", help="Verification command for Ralph Loop. Repeatable."),
27
43
  json_output: bool = typer.Option(False, "--json", help="Emit JSON."),
28
44
  ) -> None:
29
45
  """Initialize if needed, write a task, refresh context, and show next steps."""
@@ -44,9 +60,39 @@ def register(app: typer.Typer) -> None:
44
60
  if pack_only:
45
61
  start_args.append("--pack-only")
46
62
  stages.append(_run("start", cli_module_argv(*start_args), root))
47
- if stages[-1]["returncode"] == 0 and not no_next:
63
+ if stages[-1]["returncode"] == 0 and not no_next and not run_loop_requested and not dry_run:
48
64
  stages.append(_run("next", cli_module_argv("next"), root))
49
- _finish(stages, json_output)
65
+ if stages[-1]["returncode"] != 0:
66
+ _finish(stages, json_output)
67
+
68
+ loop_plan = None
69
+ loop_summary = None
70
+ if run_loop_requested or dry_run:
71
+ cfg = load_config(root)
72
+ state = initialize_loop(
73
+ root,
74
+ task_text,
75
+ cfg.loop,
76
+ runner_override=runner,
77
+ max_iterations_override=max_iterations,
78
+ verification_overrides=list(verify) if verify else None,
79
+ )
80
+ if dry_run:
81
+ loop_plan = dry_run_plan(root, state).model_dump(mode="json")
82
+ _finish(stages, json_output, loop_plan=loop_plan)
83
+ return
84
+ if not state.runner:
85
+ console.print("[red]Ralph Loop runner missing.[/] Set [loop].runner or pass --runner.")
86
+ raise typer.Exit(1)
87
+ loop_summary = run_loop(
88
+ root,
89
+ state,
90
+ refresh=lambda: _refresh_loop_context(root, agent, mode, budget, resolve_thread_option(thread)),
91
+ ).model_dump(mode="json")
92
+ if loop_summary["status"] != "ready_to_finish":
93
+ _finish(stages, json_output, loop_summary=loop_summary)
94
+ raise typer.Exit(1)
95
+ _finish(stages, json_output, loop_plan=loop_plan, loop_summary=loop_summary)
50
96
 
51
97
  @app.command("finish")
52
98
  def finish(
@@ -64,6 +110,16 @@ def register(app: typer.Typer) -> None:
64
110
  """Run finish checks, capture benchmark evidence, and mark work done."""
65
111
  root = _root()
66
112
  stages: list[dict[str, Any]] = []
113
+ loop_state = load_loop_state(root)
114
+ cfg = load_config(root)
115
+ finish_task = task or _read_task(root, thread) or (loop_state.task if loop_state else "")
116
+ loop_applies = loop_state is not None and cfg.loop.enabled and (not finish_task or finish_task == loop_state.task)
117
+ if loop_applies:
118
+ blockers = _loop_finish_blockers(root, cfg.loop, loop_state, thread)
119
+ if blockers:
120
+ _finish_blocked(blockers, json_output)
121
+ raise typer.Exit(1)
122
+
67
123
  if not skip_diagnosis:
68
124
  stages.append(_run("diagnose-selection", cli_module_argv("diagnose-selection", "--write"), root))
69
125
  if not skip_benchmark_capture and since:
@@ -84,6 +140,8 @@ def register(app: typer.Typer) -> None:
84
140
  if thread_id:
85
141
  state_args.extend(["--thread", thread_id])
86
142
  stages.append(_run("state-done", cli_module_argv(*state_args), root))
143
+ if stages[-1]["returncode"] == 0 and loop_applies:
144
+ mark_done(root, summary)
87
145
  if archive_thread and thread_id and stages[-1]["returncode"] == 0:
88
146
  stages.append(_run("threads-archive", cli_module_argv("threads", "archive", thread_id, "--summary", summary), root))
89
147
  _finish(stages, json_output)
@@ -100,10 +158,21 @@ def _run(name: str, command: list[str], root: Path) -> dict[str, Any]:
100
158
  }
101
159
 
102
160
 
103
- def _finish(stages: list[dict[str, Any]], json_output: bool) -> None:
161
+ def _finish(
162
+ stages: list[dict[str, Any]],
163
+ json_output: bool,
164
+ *,
165
+ loop_plan: dict[str, Any] | None = None,
166
+ loop_summary: dict[str, Any] | None = None,
167
+ ) -> None:
104
168
  passed = all(stage["returncode"] == 0 for stage in stages)
105
169
  if json_output:
106
- typer.echo(json.dumps({"passed": passed, "stages": stages}, indent=2, sort_keys=True))
170
+ payload: dict[str, Any] = {"passed": passed, "stages": stages}
171
+ if loop_plan is not None:
172
+ payload["loop_plan"] = loop_plan
173
+ if loop_summary is not None:
174
+ payload["loop_summary"] = loop_summary
175
+ typer.echo(json.dumps(payload, indent=2, sort_keys=True))
107
176
  else:
108
177
  for stage in stages:
109
178
  marker = "[green]✓[/]" if stage["returncode"] == 0 else "[red]✗[/]"
@@ -112,10 +181,39 @@ def _finish(stages: list[dict[str, Any]], json_output: bool) -> None:
112
181
  console.print(f" rerun: [bold]{stage['command']}[/]")
113
182
  if stage.get("detail"):
114
183
  console.print(f" [dim]{stage['detail']}[/]")
184
+ if loop_plan is not None:
185
+ console.print(f"[green]✓[/] Ralph Loop dry run: {loop_plan['next_action']}")
186
+ if loop_summary is not None:
187
+ marker = "[green]✓[/]" if loop_summary.get("status") == "ready_to_finish" else "[yellow]![/]"
188
+ console.print(f"{marker} Ralph Loop {loop_summary.get('status')}: {loop_summary.get('reason') or loop_summary.get('next_command')}")
115
189
  if not passed:
116
190
  raise typer.Exit(1)
117
191
 
118
192
 
193
+ def _loop_finish_blockers(root: Path, loop_cfg, loop_state, thread: str) -> list[dict[str, Any]]:
194
+ blockers = [blocker.model_dump(mode="json") for blocker in finish_blockers(root, loop_cfg, loop_state)]
195
+ fresh, reason = _context_is_fresh(root, thread_id=resolve_thread_option(thread))
196
+ if not fresh:
197
+ blockers.append(
198
+ {
199
+ "kind": "stale_context",
200
+ "message": f"Context is stale: {reason}",
201
+ "command": "agentpack guard --agent auto --repair-stale --refresh-context",
202
+ }
203
+ )
204
+ return blockers
205
+
206
+
207
+ def _finish_blocked(blockers: list[dict[str, Any]], json_output: bool) -> None:
208
+ if json_output:
209
+ typer.echo(json.dumps({"passed": False, "stages": [], "loop_blockers": blockers}, indent=2, sort_keys=True))
210
+ return
211
+ console.print("[red]Ralph Loop completion blockers:[/]")
212
+ for blocker in blockers:
213
+ console.print(f" [yellow]![/] {blocker['message']}")
214
+ console.print(f" Run: [bold]{blocker['command']}[/]")
215
+
216
+
119
217
  def _read_task(root: Path, thread: str) -> str:
120
218
  thread_id = resolve_thread_option(thread)
121
219
  if thread_id:
@@ -125,3 +223,10 @@ def _read_task(root: Path, thread: str) -> str:
125
223
  if not path.exists():
126
224
  return ""
127
225
  return path.read_text(encoding="utf-8").strip().splitlines()[0].strip()
226
+
227
+
228
+ def _refresh_loop_context(root: Path, agent: str, mode: str, budget: int, thread_id: str | None) -> LoopCommandResult:
229
+ stats = run_refresh(root, agent, mode, budget, thread_id=thread_id)
230
+ if stats is None:
231
+ return LoopCommandResult(command="agentpack guard --repair-stale --refresh-context", returncode=1, output_excerpt="context refresh failed")
232
+ return LoopCommandResult(command="agentpack guard --repair-stale --refresh-context", returncode=0, output_excerpt=json.dumps(stats, sort_keys=True))
@@ -67,6 +67,21 @@ class LearningConfig(BaseModel):
67
67
  min_groundedness_score: int = 70
68
68
 
69
69
 
70
+ class LoopConfig(BaseModel):
71
+ enabled: bool = True
72
+ runner: str = ""
73
+ max_iterations: int = 10
74
+ verification_commands: list[str] = Field(default_factory=list)
75
+ require_verification: bool = True
76
+ require_progress_update: bool = True
77
+ require_clean_tree: bool = True
78
+ auto_commit: bool = False
79
+ auto_push: bool = False
80
+ runner_timeout_seconds: int = 600
81
+ verification_timeout_seconds: int = 600
82
+ max_repeated_failures: int = 3
83
+
84
+
70
85
  class HooksConfig(BaseModel):
71
86
  task_switch_detection: bool = True
72
87
  task_switch_min_terms: int = 1
@@ -137,6 +152,7 @@ class Config(BaseModel):
137
152
  context_lite: LiteContextConfig = Field(default_factory=LiteContextConfig)
138
153
  summary: SummaryConfig = Field(default_factory=SummaryConfig)
139
154
  learning: LearningConfig = Field(default_factory=LearningConfig)
155
+ loop: LoopConfig = Field(default_factory=LoopConfig)
140
156
  hooks: HooksConfig = Field(default_factory=HooksConfig)
141
157
  skills: SkillsConfig = Field(default_factory=SkillsConfig)
142
158
  agents: AgentsConfig = Field(default_factory=AgentsConfig)
@@ -199,6 +215,20 @@ max_cards = 5
199
215
  max_quiz_questions = 5
200
216
  min_groundedness_score = 70
201
217
 
218
+ [loop]
219
+ enabled = true
220
+ runner = ""
221
+ max_iterations = 10
222
+ verification_commands = []
223
+ require_verification = true
224
+ require_progress_update = true
225
+ require_clean_tree = true
226
+ auto_commit = false
227
+ auto_push = false
228
+ runner_timeout_seconds = 600
229
+ verification_timeout_seconds = 600
230
+ max_repeated_failures = 3
231
+
202
232
  [hooks]
203
233
  # Claude UserPromptSubmit can detect a clearly different coding prompt,
204
234
  # update .agentpack/task.md, and repack even if files did not change.