agentpack-cli 0.1.23__tar.gz → 0.1.25__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 (86) hide show
  1. agentpack_cli-0.1.25/.gitignore +33 -0
  2. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/PKG-INFO +10 -3
  3. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/README.md +9 -2
  4. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/pyproject.toml +1 -1
  5. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/__init__.py +1 -1
  6. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/analysis/ranking.py +27 -3
  7. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/application/pack_service.py +172 -12
  8. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/benchmark.py +108 -19
  9. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/claude_cmd.py +1 -0
  10. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/diff.py +7 -1
  11. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/doctor.py +74 -1
  12. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/explain.py +52 -0
  13. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/init.py +56 -0
  14. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/pack.py +35 -8
  15. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/scan.py +7 -1
  16. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/stats.py +153 -2
  17. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/status.py +7 -1
  18. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/summarize.py +7 -1
  19. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/watch.py +1 -0
  20. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/context_pack.py +16 -1
  21. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/git.py +32 -0
  22. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/models.py +4 -1
  23. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/scanner.py +4 -0
  24. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/mcp_server.py +33 -1
  25. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/renderers/markdown.py +20 -0
  26. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/session/state.py +2 -0
  27. agentpack_cli-0.1.23/.gitignore +0 -21
  28. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/LICENSE +0 -0
  29. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/adapters/__init__.py +0 -0
  30. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/adapters/antigravity.py +0 -0
  31. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/adapters/base.py +0 -0
  32. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/adapters/claude.py +0 -0
  33. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/adapters/codex.py +0 -0
  34. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/adapters/cursor.py +0 -0
  35. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/adapters/detect.py +0 -0
  36. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/adapters/generic.py +0 -0
  37. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/adapters/windsurf.py +0 -0
  38. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/analysis/__init__.py +0 -0
  39. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/analysis/dependency_graph.py +0 -0
  40. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/analysis/go_imports.py +0 -0
  41. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/analysis/java_imports.py +0 -0
  42. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/analysis/js_ts_imports.py +0 -0
  43. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/analysis/python_imports.py +0 -0
  44. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/analysis/rust_imports.py +0 -0
  45. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/analysis/symbols.py +0 -0
  46. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/analysis/tests.py +0 -0
  47. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/application/__init__.py +0 -0
  48. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/cli.py +0 -0
  49. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/__init__.py +0 -0
  50. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/_shared.py +0 -0
  51. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/hook_cmd.py +0 -0
  52. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/install.py +0 -0
  53. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/mcp_cmd.py +0 -0
  54. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/monitor.py +0 -0
  55. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/commands/quickstart.py +0 -0
  56. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/__init__.py +0 -0
  57. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/bootstrap.py +0 -0
  58. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/cache.py +0 -0
  59. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/config.py +0 -0
  60. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/diff.py +0 -0
  61. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/git_hooks.py +0 -0
  62. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/global_install.py +0 -0
  63. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/ignore.py +0 -0
  64. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/merkle.py +0 -0
  65. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/redactor.py +0 -0
  66. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/snapshot.py +0 -0
  67. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/token_estimator.py +0 -0
  68. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/core/vscode_tasks.py +0 -0
  69. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/data/agentpack.md +0 -0
  70. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/installers/__init__.py +0 -0
  71. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/installers/antigravity.py +0 -0
  72. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/installers/claude.py +0 -0
  73. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/installers/codex.py +0 -0
  74. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/installers/cursor.py +0 -0
  75. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/installers/windsurf.py +0 -0
  76. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/integrations/__init__.py +0 -0
  77. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/integrations/git_hooks.py +0 -0
  78. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/integrations/global_install.py +0 -0
  79. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/integrations/vscode_tasks.py +0 -0
  80. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/renderers/__init__.py +0 -0
  81. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/renderers/compact.py +0 -0
  82. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/renderers/receipts.py +0 -0
  83. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/session/__init__.py +0 -0
  84. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/summaries/__init__.py +0 -0
  85. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/summaries/base.py +0 -0
  86. {agentpack_cli-0.1.23 → agentpack_cli-0.1.25}/src/agentpack/summaries/offline.py +0 -0
@@ -0,0 +1,33 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.egg
8
+
9
+ .venv/
10
+ venv/
11
+ env/
12
+
13
+ # agentpack:start
14
+ # AgentPack generated context/cache (safe to ignore)
15
+ .agentpack/cache/
16
+ .agentpack/snapshots/
17
+ .agentpack/context*
18
+ .agentpack/metrics.jsonl
19
+ .agentpack/pack_metadata.json
20
+ .agentpack/activity.log
21
+ .agentpack/.gitignore
22
+ .agentpack/.mcp_reminded
23
+ .agentpack/session.json
24
+ .agentpack/task.md
25
+ .agentpack/benchmark_results.jsonl
26
+ .agent/skills/agentpack/
27
+ # agentpack:end
28
+
29
+ .pytest_cache/
30
+ .mypy_cache/
31
+ .ruff_cache/
32
+
33
+ *.dist-info/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentpack-cli
3
- Version: 0.1.23
3
+ Version: 0.1.25
4
4
  Summary: Task-aware context packing for AI coding agents — Claude, Cursor, Windsurf, Codex, and Antigravity
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -44,7 +44,7 @@ Description-Content-Type: text/markdown
44
44
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
45
45
  [![CI](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml/badge.svg)](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml)
46
46
 
47
- > **Status: alpha (v0.1.23).** Works, tested, used in real sessions. Python and JavaScript/TypeScript are the best-supported languages. Not yet validated across a wide range of repos. API may change before 1.0.
47
+ > **Status: alpha (v0.1.25).** Works, tested, used in real sessions. Python and JavaScript/TypeScript are the best-supported languages. Not yet validated across a wide range of repos. API may change before 1.0.
48
48
  >
49
49
  > **Platform note:** macOS and Linux are fully supported. Windows support is not yet implemented (git hooks use POSIX shell; the Claude Code session hooks use `python3`/`rm -f`). Contributions welcome.
50
50
 
@@ -642,6 +642,7 @@ agentpack init --share-cache # commit cache/ to git for team sharing
642
642
 
643
643
  Creates:
644
644
  ```
645
+ .gitignore # patched idempotently with AgentPack generated artifacts
645
646
  .agentignore # gitignore-style file exclusion rules
646
647
  .agentpack/
647
648
  config.toml # configuration (safe to commit)
@@ -896,6 +897,7 @@ Mode comparison: fix auth token expiry
896
897
  [[cases]]
897
898
  task = "fix auth token expiry"
898
899
  mode = "balanced"
900
+ task_type = "backend-api"
899
901
  expected_files = [
900
902
  "src/auth/token.py",
901
903
  "src/auth/session.py",
@@ -909,6 +911,8 @@ expected_files = [
909
911
 
910
912
  Use `--misses` when recall is low. It prints each expected file that was not selected with status, rank, score, and scoring reasons, which helps separate ignored files, budget cuts, low scores, and missing dependency signals.
911
913
 
914
+ Add `task_type` to group results by workflow area. Benchmark summaries report average precision, recall, F1, and token noise by type, so a repo can show "backend-api is good, frontend-web is noisy" instead of hiding that under one aggregate.
915
+
912
916
  ---
913
917
 
914
918
  ### `agentpack scan`
@@ -949,7 +953,7 @@ agentpack benchmark --compare --misses
949
953
 
950
954
  `--sample-fixtures` runs bundled FastAPI, Next.js, and mixed Python/TypeScript fixture evals from an AgentPack source checkout. It is a smoke test, not a claim about your repo.
951
955
 
952
- For an 8+ usefulness signal, use `benchmark.toml` with real third-party or customer-style repos: 5-20 historical tasks, the files actually changed for each task, and `--compare` results for recall, F1, rank@K, and token noise. That is better than trusting generic benchmarks because it tells you whether AgentPack selects the files that matter in code the package has never seen.
956
+ For an 8+ usefulness signal, use `benchmark.toml` with real third-party or customer-style repos: 5-20 historical tasks, `task_type` labels, the files actually changed for each task, and `--compare` results for recall, F1, rank@K, and token noise. That is better than trusting generic benchmarks because it tells you whether AgentPack selects the files that matter in code the package has never seen.
953
957
 
954
958
  ---
955
959
 
@@ -1111,8 +1115,11 @@ Works like `.gitignore`. Default rules exclude:
1111
1115
  .agentignore ✓ commit
1112
1116
  .agentpack/config.toml ✓ commit
1113
1117
  .agentpack/cache/ ✓ commit if --share-cache (recommended for teams)
1118
+ .agentpack/.gitignore ✗ gitignored
1114
1119
  .agentpack/snapshots/ ✗ gitignored
1115
1120
  .agentpack/context.* ✗ gitignored
1121
+ .agentpack/task.md ✗ gitignored (local current task)
1122
+ .agent/skills/agentpack/ ✗ gitignored (generated Antigravity context)
1116
1123
  ```
1117
1124
 
1118
1125
  ---
@@ -5,7 +5,7 @@
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![CI](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml/badge.svg)](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml)
7
7
 
8
- > **Status: alpha (v0.1.23).** Works, tested, used in real sessions. Python and JavaScript/TypeScript are the best-supported languages. Not yet validated across a wide range of repos. API may change before 1.0.
8
+ > **Status: alpha (v0.1.25).** Works, tested, used in real sessions. Python and JavaScript/TypeScript are the best-supported languages. Not yet validated across a wide range of repos. API may change before 1.0.
9
9
  >
10
10
  > **Platform note:** macOS and Linux are fully supported. Windows support is not yet implemented (git hooks use POSIX shell; the Claude Code session hooks use `python3`/`rm -f`). Contributions welcome.
11
11
 
@@ -603,6 +603,7 @@ agentpack init --share-cache # commit cache/ to git for team sharing
603
603
 
604
604
  Creates:
605
605
  ```
606
+ .gitignore # patched idempotently with AgentPack generated artifacts
606
607
  .agentignore # gitignore-style file exclusion rules
607
608
  .agentpack/
608
609
  config.toml # configuration (safe to commit)
@@ -857,6 +858,7 @@ Mode comparison: fix auth token expiry
857
858
  [[cases]]
858
859
  task = "fix auth token expiry"
859
860
  mode = "balanced"
861
+ task_type = "backend-api"
860
862
  expected_files = [
861
863
  "src/auth/token.py",
862
864
  "src/auth/session.py",
@@ -870,6 +872,8 @@ expected_files = [
870
872
 
871
873
  Use `--misses` when recall is low. It prints each expected file that was not selected with status, rank, score, and scoring reasons, which helps separate ignored files, budget cuts, low scores, and missing dependency signals.
872
874
 
875
+ Add `task_type` to group results by workflow area. Benchmark summaries report average precision, recall, F1, and token noise by type, so a repo can show "backend-api is good, frontend-web is noisy" instead of hiding that under one aggregate.
876
+
873
877
  ---
874
878
 
875
879
  ### `agentpack scan`
@@ -910,7 +914,7 @@ agentpack benchmark --compare --misses
910
914
 
911
915
  `--sample-fixtures` runs bundled FastAPI, Next.js, and mixed Python/TypeScript fixture evals from an AgentPack source checkout. It is a smoke test, not a claim about your repo.
912
916
 
913
- For an 8+ usefulness signal, use `benchmark.toml` with real third-party or customer-style repos: 5-20 historical tasks, the files actually changed for each task, and `--compare` results for recall, F1, rank@K, and token noise. That is better than trusting generic benchmarks because it tells you whether AgentPack selects the files that matter in code the package has never seen.
917
+ For an 8+ usefulness signal, use `benchmark.toml` with real third-party or customer-style repos: 5-20 historical tasks, `task_type` labels, the files actually changed for each task, and `--compare` results for recall, F1, rank@K, and token noise. That is better than trusting generic benchmarks because it tells you whether AgentPack selects the files that matter in code the package has never seen.
914
918
 
915
919
  ---
916
920
 
@@ -1072,8 +1076,11 @@ Works like `.gitignore`. Default rules exclude:
1072
1076
  .agentignore ✓ commit
1073
1077
  .agentpack/config.toml ✓ commit
1074
1078
  .agentpack/cache/ ✓ commit if --share-cache (recommended for teams)
1079
+ .agentpack/.gitignore ✗ gitignored
1075
1080
  .agentpack/snapshots/ ✗ gitignored
1076
1081
  .agentpack/context.* ✗ gitignored
1082
+ .agentpack/task.md ✗ gitignored (local current task)
1083
+ .agent/skills/agentpack/ ✗ gitignored (generated Antigravity context)
1077
1084
  ```
1078
1085
 
1079
1086
  ---
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentpack-cli"
3
- version = "0.1.23"
3
+ version = "0.1.25"
4
4
  description = "Task-aware context packing for AI coding agents — Claude, Cursor, Windsurf, Codex, and Antigravity"
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.1.23"
3
+ __version__ = "0.1.25"
@@ -16,6 +16,16 @@ _STOPWORDS = {
16
16
  "use", "using", "used", "how", "what", "when", "where", "why",
17
17
  }
18
18
 
19
+ _GENERIC_TASK_TERMS = {
20
+ "add", "added", "change", "changed", "changes", "clean", "cleanup",
21
+ "code", "commit", "context", "debug", "dev", "development", "doc",
22
+ "docs", "eval", "evals", "feature", "fix", "freshness", "general",
23
+ "impl", "implement", "implementation", "improve", "issue", "metric", "metrics",
24
+ "noise", "noisy", "package", "pack", "packs", "release", "repo",
25
+ "source", "sync", "task", "tasks", "test", "tests", "update", "use",
26
+ "useful", "usefulness", "version", "workflow", "workflows",
27
+ }
28
+
19
29
  _CONCEPT_MAP: dict[str, frozenset[str]] = {
20
30
  # rate limiting
21
31
  "rate": frozenset({"throttle", "ratelimit", "leaky", "bucket", "debounce", "backoff", "quota"}),
@@ -219,15 +229,18 @@ def extract_keyword_weights(task: str) -> dict[str, float]:
219
229
  continue
220
230
  if word in _STOPWORDS:
221
231
  continue
222
- _add_keyword_weight(keyword_weights, word, 1.0)
232
+ literal_weight = 0.25 if word in _GENERIC_TASK_TERMS else 1.0
233
+ _add_keyword_weight(keyword_weights, word, literal_weight)
223
234
  if word in _VARIANTS:
224
- _add_keyword_weight(keyword_weights, _VARIANTS[word], 0.75)
235
+ variant = _VARIANTS[word]
236
+ variant_weight = 0.25 if variant in _GENERIC_TASK_TERMS else min(0.75, literal_weight)
237
+ _add_keyword_weight(keyword_weights, variant, variant_weight)
225
238
 
226
239
  # Expand via concept map one level only. Expanded concepts are weaker than
227
240
  # literal task words so broad terms like "task" do not dominate ranking.
228
241
  expanded: dict[str, float] = {}
229
242
  for kw in keyword_weights:
230
- if kw in _CONCEPT_MAP:
243
+ if kw in _CONCEPT_MAP and kw not in _GENERIC_TASK_TERMS:
231
244
  for synonym in _CONCEPT_MAP[kw]:
232
245
  _add_keyword_weight(expanded, synonym, 0.35)
233
246
  if synonym in _VARIANTS:
@@ -237,6 +250,17 @@ def extract_keyword_weights(task: str) -> dict[str, float]:
237
250
  return keyword_weights
238
251
 
239
252
 
253
+ def generic_task_term_ratio(task: str) -> float:
254
+ words = [
255
+ word for word in re.split(r"[^a-zA-Z0-9]+", task.lower())
256
+ if len(word) >= 3 and word not in _STOPWORDS
257
+ ]
258
+ if not words:
259
+ return 0.0
260
+ generic = sum(1 for word in words if word in _GENERIC_TASK_TERMS)
261
+ return generic / len(words)
262
+
263
+
240
264
  def extract_keywords(task: str) -> set[str]:
241
265
  return set(extract_keyword_weights(task))
242
266
 
@@ -16,12 +16,14 @@ from agentpack.core import git
16
16
  from agentpack.core.context_pack import select_files, save_pack_metadata
17
17
  from agentpack.core.models import ContextPack, DependencyGraph, FileInfo, ScanResult, SelectedFile, Receipt
18
18
  from agentpack.core.token_estimator import estimate_tokens
19
+ from agentpack.renderers.markdown import render_generic
19
20
  from agentpack.analysis.ranking import (
20
21
  score_files,
21
22
  extract_keyword_weights,
22
23
  enrich_keyword_weights_from_files,
23
24
  boost_paired_tests,
24
25
  boost_cross_layer_related,
26
+ generic_task_term_ratio,
25
27
  )
26
28
  from agentpack.analysis.tests import find_related_tests
27
29
  from agentpack.analysis import dependency_graph as dep_graph_mod
@@ -37,6 +39,7 @@ class PackRequest:
37
39
  budget: int
38
40
  since: str | None
39
41
  refresh: bool
42
+ task_source: str = "explicit"
40
43
 
41
44
 
42
45
  @dataclass
@@ -57,6 +60,7 @@ class ChangeSet:
57
60
  all_changed: set[str]
58
61
  git_staged: set[str]
59
62
  recently_modified: list[str]
63
+ source: str
60
64
  current_snap: dict[str, Any] = field(default_factory=dict)
61
65
 
62
66
 
@@ -64,6 +68,7 @@ class ChangeSet:
64
68
  class RankResult:
65
69
  """Result of keyword extraction and file scoring."""
66
70
  keywords: set[str]
71
+ generic_ratio: float
67
72
  scored: list[tuple[Any, float, list[str]]]
68
73
 
69
74
 
@@ -80,6 +85,8 @@ class PackPlan:
80
85
  git_staged: set[str]
81
86
  recently_modified: list[str]
82
87
  keywords: set[str]
88
+ generic_task_ratio: float
89
+ changed_files_source: str
83
90
  scored: list[tuple[Any, float, list[str]]]
84
91
  selected: list[SelectedFile]
85
92
  receipts: list[Receipt]
@@ -119,6 +126,7 @@ class ChangeDetector:
119
126
  all_changed=changed_from_snap | git_changed,
120
127
  git_staged=git_staged,
121
128
  recently_modified=recently_modified,
129
+ source=_change_source(root, since, changed_from_snap, git_changed),
122
130
  current_snap=current_snap,
123
131
  )
124
132
 
@@ -140,6 +148,7 @@ class FileRanker:
140
148
  keyword_weights = extract_keyword_weights(task)
141
149
  keyword_weights = enrich_keyword_weights_from_files(keyword_weights, changes.all_changed, packable)
142
150
  keywords = set(keyword_weights)
151
+ generic_ratio = generic_task_term_ratio(task)
143
152
  all_paths = {f.path for f in packable}
144
153
 
145
154
  for fi in packable:
@@ -165,7 +174,7 @@ class FileRanker:
165
174
  )
166
175
  scored = boost_cross_layer_related(scored, keyword_weights, weights=cfg.scoring)
167
176
  scored = boost_paired_tests(scored, weights=cfg.scoring)
168
- return RankResult(keywords=keywords, scored=scored)
177
+ return RankResult(keywords=keywords, generic_ratio=generic_ratio, scored=scored)
169
178
 
170
179
 
171
180
  class PackPlanner:
@@ -185,6 +194,7 @@ class PackPlanner:
185
194
  previous_snapshot=previous_snap,
186
195
  include_globs=cfg.project.include_globs or None,
187
196
  exclude_globs=cfg.project.exclude_globs or None,
197
+ always_skip_paths=AdapterRegistry.generated_output_paths(root, cfg),
188
198
  )
189
199
  phase_times["scan"] = time.perf_counter() - t0
190
200
 
@@ -217,8 +227,8 @@ class PackPlanner:
217
227
  budget=effective_budget,
218
228
  max_file_tokens=cfg.context.max_file_tokens,
219
229
  keywords=rank_result.keywords,
220
- min_summary_score=cfg.context.min_summary_score,
221
- max_summary_files=_summary_cap_for_mode(cfg, request.mode),
230
+ min_summary_score=_summary_score_floor(cfg, rank_result.generic_ratio),
231
+ max_summary_files=_summary_cap_for_mode(cfg, request.mode, rank_result.generic_ratio),
222
232
  )
223
233
  phase_times["select"] = time.perf_counter() - t0
224
234
 
@@ -233,6 +243,8 @@ class PackPlanner:
233
243
  git_staged=changes.git_staged,
234
244
  recently_modified=changes.recently_modified,
235
245
  keywords=rank_result.keywords,
246
+ generic_task_ratio=rank_result.generic_ratio,
247
+ changed_files_source=changes.source,
236
248
  scored=rank_result.scored,
237
249
  selected=selected,
238
250
  receipts=receipts,
@@ -245,7 +257,7 @@ class AdapterRegistry:
245
257
  """Maps agent names to adapter instances; extensible without touching PackService."""
246
258
 
247
259
  @staticmethod
248
- def get(agent: str, cfg: Any) -> Any:
260
+ def _factories(cfg: Any) -> dict[str, Any]:
249
261
  from agentpack.adapters.antigravity import AntigravityAdapter
250
262
  from agentpack.adapters.claude import ClaudeAdapter
251
263
  from agentpack.adapters.codex import CodexAdapter
@@ -253,15 +265,33 @@ class AdapterRegistry:
253
265
  from agentpack.adapters.windsurf import WindsurfAdapter
254
266
  from agentpack.adapters.generic import GenericAdapter
255
267
 
256
- adapters = {
268
+ return {
257
269
  "antigravity": lambda: AntigravityAdapter(),
258
270
  "claude": lambda: ClaudeAdapter(cfg.agents.claude.output),
259
271
  "cursor": lambda: CursorAdapter(cfg.agents.generic.output),
260
272
  "windsurf": lambda: WindsurfAdapter(cfg.agents.generic.output),
261
273
  "codex": lambda: CodexAdapter(cfg.agents.generic.output),
274
+ "generic": lambda: GenericAdapter(cfg.agents.generic.output),
262
275
  }
276
+
277
+ @staticmethod
278
+ def get(agent: str, cfg: Any) -> Any:
279
+ from agentpack.adapters.generic import GenericAdapter
280
+
281
+ adapters = AdapterRegistry._factories(cfg)
263
282
  return adapters.get(agent, lambda: GenericAdapter(cfg.agents.generic.output))()
264
283
 
284
+ @staticmethod
285
+ def generated_output_paths(root: Path, cfg: Any) -> set[str]:
286
+ paths: set[str] = set()
287
+ for factory in AdapterRegistry._factories(cfg).values():
288
+ try:
289
+ out_path = factory().output_path(root)
290
+ paths.add(str(out_path.relative_to(root)).replace("\\", "/"))
291
+ except (OSError, ValueError):
292
+ continue
293
+ return paths
294
+
265
295
 
266
296
  class PackService:
267
297
  """Materializes a plan from PackPlanner into a written context file."""
@@ -279,6 +309,13 @@ class PackService:
279
309
  saving_pct = (1 - packed_tokens / all_tokens) * 100 if all_tokens > 0 else 0.0
280
310
 
281
311
  all_redaction_warnings = [w for sf in plan.selected for w in sf.redaction_warnings]
312
+ freshness = _build_freshness_metadata(
313
+ root,
314
+ request=request,
315
+ plan=plan,
316
+ snapshot_root_hash=plan.current_snap["root_hash"],
317
+ )
318
+ freshness_warnings = _freshness_warnings(root, request, freshness)
282
319
 
283
320
  pack_obj = ContextPack(
284
321
  task=request.task,
@@ -294,12 +331,15 @@ class PackService:
294
331
  receipts=plan.receipts if cfg.context.include_receipts else [],
295
332
  redaction_warnings=all_redaction_warnings,
296
333
  stale=False,
334
+ freshness=freshness,
335
+ freshness_warnings=freshness_warnings,
297
336
  )
298
337
 
299
338
  adapter = AdapterRegistry.get(request.agent, cfg)
300
339
 
301
340
  t0 = time.perf_counter()
302
341
  out_path = adapter.write(pack_obj, root)
342
+ _write_canonical_context(pack_obj, root, out_path)
303
343
  plan.phase_times["render"] = time.perf_counter() - t0
304
344
 
305
345
  save_snapshot(plan.current_snap, root)
@@ -312,6 +352,9 @@ class PackService:
312
352
  mode=request.mode,
313
353
  budget=plan.budget,
314
354
  token_estimate=packed_tokens,
355
+ freshness=freshness,
356
+ freshness_warnings=freshness_warnings,
357
+ selected_files=_selected_file_metadata(plan.selected),
315
358
  )
316
359
  excluded_receipts = [r for r in plan.receipts if r.action == "excluded"]
317
360
  # Budget-cut: files that scored OK but didn't fit — more useful signal than "score too low"
@@ -347,6 +390,33 @@ class PackService:
347
390
  )
348
391
 
349
392
 
393
+ def _write_canonical_context(pack: ContextPack, root: Path, out_path: Path) -> None:
394
+ """Keep .agentpack/context.md fresh even when the target agent writes elsewhere."""
395
+ canonical_path = root / ".agentpack" / "context.md"
396
+ try:
397
+ if out_path.resolve() == canonical_path.resolve():
398
+ return
399
+ except OSError:
400
+ if out_path == canonical_path:
401
+ return
402
+ canonical_path.parent.mkdir(parents=True, exist_ok=True)
403
+ canonical_path.write_text(render_generic(pack), encoding="utf-8")
404
+
405
+
406
+ def _selected_file_metadata(selected: list[SelectedFile]) -> list[dict[str, Any]]:
407
+ return [
408
+ {
409
+ "path": sf.path,
410
+ "mode": sf.include_mode,
411
+ "score": round(sf.score, 1),
412
+ "why": sf.reasons[0] if sf.reasons else "",
413
+ "reasons": sf.reasons,
414
+ "tokens": _sf_tokens(sf),
415
+ }
416
+ for sf in selected
417
+ ]
418
+
419
+
350
420
  def _sf_tokens(sf: SelectedFile) -> int:
351
421
  if sf.content:
352
422
  return estimate_tokens(sf.content)
@@ -359,14 +429,104 @@ def _sf_tokens(sf: SelectedFile) -> int:
359
429
  return estimate_tokens("\n".join(parts)) if parts else 50
360
430
 
361
431
 
362
- def _summary_cap_for_mode(cfg: Any, mode: str) -> int:
432
+ def _summary_score_floor(cfg: Any, generic_ratio: float) -> float:
433
+ floor = cfg.context.min_summary_score
434
+ if generic_ratio >= 0.5:
435
+ return floor + 15
436
+ if generic_ratio >= 0.35:
437
+ return floor + 8
438
+ return floor
439
+
440
+
441
+ def _summary_cap_for_mode(cfg: Any, mode: str, generic_ratio: float = 0.0) -> int:
363
442
  if mode == "minimal":
364
- return cfg.context.max_summary_files_minimal
365
- if mode == "balanced":
366
- return cfg.context.max_summary_files_balanced
367
- if mode == "deep":
368
- return cfg.context.max_summary_files_deep
369
- return 0
443
+ cap = cfg.context.max_summary_files_minimal
444
+ elif mode == "balanced":
445
+ cap = cfg.context.max_summary_files_balanced
446
+ elif mode == "deep":
447
+ cap = cfg.context.max_summary_files_deep
448
+ else:
449
+ cap = 0
450
+ if cap > 0 and generic_ratio >= 0.5:
451
+ return max(8, cap // 2)
452
+ if cap > 0 and generic_ratio >= 0.35:
453
+ return max(12, int(cap * 0.75))
454
+ return cap
455
+
456
+
457
+ def _change_source(root: Path, since: str | None, snapshot_changed: set[str], git_changed: set[str]) -> str:
458
+ if not git.is_git_repo(root):
459
+ return "snapshot diff"
460
+ if since:
461
+ return f"git diff since {since} + snapshot diff"
462
+ if git_changed and snapshot_changed:
463
+ return "git working tree + snapshot diff"
464
+ if git_changed:
465
+ return "git working tree"
466
+ if snapshot_changed:
467
+ return "snapshot diff"
468
+ return "no live changes; ranking used task keywords and history"
469
+
470
+
471
+ def _task_md_body(root: Path) -> str | None:
472
+ task_md_path = root / ".agentpack" / "task.md"
473
+ if not task_md_path.exists():
474
+ return None
475
+ try:
476
+ content = task_md_path.read_text(encoding="utf-8").strip()
477
+ except OSError:
478
+ return None
479
+ lines = [ln for ln in content.splitlines() if ln.strip() and not ln.startswith("#")]
480
+ body = lines[0].strip() if lines else ""
481
+ placeholder = "Write or update the current coding task here."
482
+ if body and placeholder not in body:
483
+ return body
484
+ return None
485
+
486
+
487
+ def _build_freshness_metadata(
488
+ root: Path,
489
+ *,
490
+ request: PackRequest,
491
+ plan: PackPlan,
492
+ snapshot_root_hash: str,
493
+ ) -> dict[str, Any]:
494
+ dirty = git.dirty_files(root) if git.is_git_repo(root) else set()
495
+ metadata: dict[str, Any] = {
496
+ "generated_at": datetime.now(timezone.utc).isoformat(),
497
+ "task_source": request.task_source,
498
+ "changed_files_source": plan.changed_files_source,
499
+ "snapshot_root_hash": snapshot_root_hash,
500
+ "generic_task_ratio": round(plan.generic_task_ratio, 3),
501
+ "dirty_files_count": len(dirty),
502
+ }
503
+ if git.is_git_repo(root):
504
+ metadata["git_sha"] = git.current_sha(root)
505
+ metadata["git_branch"] = git.current_branch(root)
506
+ if dirty:
507
+ metadata["dirty_files_sample"] = sorted(dirty)[:8]
508
+ task_md = _task_md_body(root)
509
+ if task_md:
510
+ metadata["task_md"] = task_md
511
+ return metadata
512
+
513
+
514
+ def _freshness_warnings(root: Path, request: PackRequest, freshness: dict[str, Any]) -> list[str]:
515
+ warnings: list[str] = []
516
+ task_md = freshness.get("task_md")
517
+ if task_md and task_md != request.task:
518
+ warnings.append(
519
+ ".agentpack/task.md differs from the packed task; rerun with --task auto if task.md should win."
520
+ )
521
+ if freshness.get("changed_files_source") == "no live changes; ranking used task keywords and history":
522
+ warnings.append("No live changed files were detected; treat selected files as keyword-based hints.")
523
+ if freshness.get("generic_task_ratio", 0) >= 0.5:
524
+ warnings.append("Task terms are broad/generic; pack tightened weak-summary selection.")
525
+ saved_sha = freshness.get("git_sha")
526
+ current_sha = git.current_sha(root) if git.is_git_repo(root) else None
527
+ if saved_sha and current_sha and saved_sha != current_sha:
528
+ warnings.append("Git HEAD changed since this pack was generated.")
529
+ return warnings
370
530
 
371
531
 
372
532
  def _load_last_record(metrics_path: Path) -> dict[str, Any] | None: