agentpack-cli 0.1.24__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.24 → agentpack_cli-0.1.25}/PKG-INFO +6 -2
  3. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/README.md +5 -1
  4. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/pyproject.toml +1 -1
  5. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/__init__.py +1 -1
  6. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/application/pack_service.py +51 -2
  7. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/claude_cmd.py +1 -0
  8. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/diff.py +7 -1
  9. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/doctor.py +74 -1
  10. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/explain.py +52 -0
  11. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/init.py +56 -0
  12. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/pack.py +24 -3
  13. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/scan.py +7 -1
  14. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/stats.py +153 -2
  15. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/status.py +7 -1
  16. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/summarize.py +7 -1
  17. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/watch.py +1 -0
  18. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/context_pack.py +2 -0
  19. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/scanner.py +4 -0
  20. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/session/state.py +2 -0
  21. agentpack_cli-0.1.24/.gitignore +0 -21
  22. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/LICENSE +0 -0
  23. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/__init__.py +0 -0
  24. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/antigravity.py +0 -0
  25. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/base.py +0 -0
  26. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/claude.py +0 -0
  27. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/codex.py +0 -0
  28. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/cursor.py +0 -0
  29. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/detect.py +0 -0
  30. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/generic.py +0 -0
  31. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/windsurf.py +0 -0
  32. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/__init__.py +0 -0
  33. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/dependency_graph.py +0 -0
  34. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/go_imports.py +0 -0
  35. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/java_imports.py +0 -0
  36. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/js_ts_imports.py +0 -0
  37. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/python_imports.py +0 -0
  38. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/ranking.py +0 -0
  39. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/rust_imports.py +0 -0
  40. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/symbols.py +0 -0
  41. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/tests.py +0 -0
  42. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/application/__init__.py +0 -0
  43. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/cli.py +0 -0
  44. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/__init__.py +0 -0
  45. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/_shared.py +0 -0
  46. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/benchmark.py +0 -0
  47. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/hook_cmd.py +0 -0
  48. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/install.py +0 -0
  49. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/mcp_cmd.py +0 -0
  50. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/monitor.py +0 -0
  51. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/quickstart.py +0 -0
  52. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/__init__.py +0 -0
  53. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/bootstrap.py +0 -0
  54. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/cache.py +0 -0
  55. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/config.py +0 -0
  56. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/diff.py +0 -0
  57. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/git.py +0 -0
  58. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/git_hooks.py +0 -0
  59. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/global_install.py +0 -0
  60. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/ignore.py +0 -0
  61. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/merkle.py +0 -0
  62. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/models.py +0 -0
  63. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/redactor.py +0 -0
  64. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/snapshot.py +0 -0
  65. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/token_estimator.py +0 -0
  66. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/vscode_tasks.py +0 -0
  67. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/data/agentpack.md +0 -0
  68. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/installers/__init__.py +0 -0
  69. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/installers/antigravity.py +0 -0
  70. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/installers/claude.py +0 -0
  71. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/installers/codex.py +0 -0
  72. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/installers/cursor.py +0 -0
  73. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/installers/windsurf.py +0 -0
  74. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/integrations/__init__.py +0 -0
  75. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/integrations/git_hooks.py +0 -0
  76. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/integrations/global_install.py +0 -0
  77. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/integrations/vscode_tasks.py +0 -0
  78. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/mcp_server.py +0 -0
  79. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/renderers/__init__.py +0 -0
  80. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/renderers/compact.py +0 -0
  81. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/renderers/markdown.py +0 -0
  82. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/renderers/receipts.py +0 -0
  83. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/session/__init__.py +0 -0
  84. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/summaries/__init__.py +0 -0
  85. {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/summaries/base.py +0 -0
  86. {agentpack_cli-0.1.24 → 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.24
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.24).** 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)
@@ -1114,8 +1115,11 @@ Works like `.gitignore`. Default rules exclude:
1114
1115
  .agentignore ✓ commit
1115
1116
  .agentpack/config.toml ✓ commit
1116
1117
  .agentpack/cache/ ✓ commit if --share-cache (recommended for teams)
1118
+ .agentpack/.gitignore ✗ gitignored
1117
1119
  .agentpack/snapshots/ ✗ gitignored
1118
1120
  .agentpack/context.* ✗ gitignored
1121
+ .agentpack/task.md ✗ gitignored (local current task)
1122
+ .agent/skills/agentpack/ ✗ gitignored (generated Antigravity context)
1119
1123
  ```
1120
1124
 
1121
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.24).** 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)
@@ -1075,8 +1076,11 @@ Works like `.gitignore`. Default rules exclude:
1075
1076
  .agentignore ✓ commit
1076
1077
  .agentpack/config.toml ✓ commit
1077
1078
  .agentpack/cache/ ✓ commit if --share-cache (recommended for teams)
1079
+ .agentpack/.gitignore ✗ gitignored
1078
1080
  .agentpack/snapshots/ ✗ gitignored
1079
1081
  .agentpack/context.* ✗ gitignored
1082
+ .agentpack/task.md ✗ gitignored (local current task)
1083
+ .agent/skills/agentpack/ ✗ gitignored (generated Antigravity context)
1080
1084
  ```
1081
1085
 
1082
1086
  ---
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentpack-cli"
3
- version = "0.1.24"
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.24"
3
+ __version__ = "0.1.25"
@@ -16,6 +16,7 @@ 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,
@@ -193,6 +194,7 @@ class PackPlanner:
193
194
  previous_snapshot=previous_snap,
194
195
  include_globs=cfg.project.include_globs or None,
195
196
  exclude_globs=cfg.project.exclude_globs or None,
197
+ always_skip_paths=AdapterRegistry.generated_output_paths(root, cfg),
196
198
  )
197
199
  phase_times["scan"] = time.perf_counter() - t0
198
200
 
@@ -255,7 +257,7 @@ class AdapterRegistry:
255
257
  """Maps agent names to adapter instances; extensible without touching PackService."""
256
258
 
257
259
  @staticmethod
258
- def get(agent: str, cfg: Any) -> Any:
260
+ def _factories(cfg: Any) -> dict[str, Any]:
259
261
  from agentpack.adapters.antigravity import AntigravityAdapter
260
262
  from agentpack.adapters.claude import ClaudeAdapter
261
263
  from agentpack.adapters.codex import CodexAdapter
@@ -263,15 +265,33 @@ class AdapterRegistry:
263
265
  from agentpack.adapters.windsurf import WindsurfAdapter
264
266
  from agentpack.adapters.generic import GenericAdapter
265
267
 
266
- adapters = {
268
+ return {
267
269
  "antigravity": lambda: AntigravityAdapter(),
268
270
  "claude": lambda: ClaudeAdapter(cfg.agents.claude.output),
269
271
  "cursor": lambda: CursorAdapter(cfg.agents.generic.output),
270
272
  "windsurf": lambda: WindsurfAdapter(cfg.agents.generic.output),
271
273
  "codex": lambda: CodexAdapter(cfg.agents.generic.output),
274
+ "generic": lambda: GenericAdapter(cfg.agents.generic.output),
272
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)
273
282
  return adapters.get(agent, lambda: GenericAdapter(cfg.agents.generic.output))()
274
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
+
275
295
 
276
296
  class PackService:
277
297
  """Materializes a plan from PackPlanner into a written context file."""
@@ -319,6 +339,7 @@ class PackService:
319
339
 
320
340
  t0 = time.perf_counter()
321
341
  out_path = adapter.write(pack_obj, root)
342
+ _write_canonical_context(pack_obj, root, out_path)
322
343
  plan.phase_times["render"] = time.perf_counter() - t0
323
344
 
324
345
  save_snapshot(plan.current_snap, root)
@@ -333,6 +354,7 @@ class PackService:
333
354
  token_estimate=packed_tokens,
334
355
  freshness=freshness,
335
356
  freshness_warnings=freshness_warnings,
357
+ selected_files=_selected_file_metadata(plan.selected),
336
358
  )
337
359
  excluded_receipts = [r for r in plan.receipts if r.action == "excluded"]
338
360
  # Budget-cut: files that scored OK but didn't fit — more useful signal than "score too low"
@@ -368,6 +390,33 @@ class PackService:
368
390
  )
369
391
 
370
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
+
371
420
  def _sf_tokens(sf: SelectedFile) -> int:
372
421
  if sf.content:
373
422
  return estimate_tokens(sf.content)
@@ -30,6 +30,7 @@ def register(app: typer.Typer) -> None:
30
30
  state.last_refresh_at = _now_iso()
31
31
  state.refresh_count += 1
32
32
  state.last_task_hash = _file_hash(root / TASK_FILE)
33
+ state.last_resolved_agent = state.agent
33
34
  save_session(root, state)
34
35
  log_activity(root, f"claude cmd — {result['files']} files, {result['tokens']:,} tokens")
35
36
  else:
@@ -8,6 +8,7 @@ from agentpack.core.ignore import load_spec
8
8
  from agentpack.core.scanner import scan
9
9
  from agentpack.core.snapshot import build_snapshot, load_snapshot
10
10
  from agentpack.core.diff import diff_snapshots
11
+ from agentpack.application.pack_service import AdapterRegistry
11
12
  from agentpack.commands._shared import console, _root
12
13
 
13
14
 
@@ -19,7 +20,12 @@ def register(app: typer.Typer) -> None:
19
20
  cfg = load_config(root)
20
21
  ignore_spec = load_spec(root / cfg.project.ignore_file)
21
22
 
22
- scan_result = scan(root, ignore_spec, cfg.context.max_file_tokens)
23
+ scan_result = scan(
24
+ root,
25
+ ignore_spec,
26
+ cfg.context.max_file_tokens,
27
+ always_skip_paths=AdapterRegistry.generated_output_paths(root, cfg),
28
+ )
23
29
  current = build_snapshot(scan_result.packable)
24
30
  previous = load_snapshot(root)
25
31
  result = diff_snapshots(previous, current)
@@ -16,6 +16,7 @@ from agentpack.integrations.global_install import (
16
16
  _detect_rc_file,
17
17
  )
18
18
  from agentpack.commands._shared import console, _root
19
+ from agentpack.core.context_pack import load_pack_metadata
19
20
 
20
21
 
21
22
  def register(app: typer.Typer) -> None:
@@ -104,11 +105,11 @@ def register(app: typer.Typer) -> None:
104
105
  return
105
106
 
106
107
  config_path = root / ".agentpack" / "config.toml"
107
- context_path = root / ".agentpack" / "context.claude.md"
108
108
  if not config_path.exists():
109
109
  console.print(f" [yellow]![/] Not initialized in {root} — run: agentpack init")
110
110
  else:
111
111
  console.print(" [green]✓[/] .agentpack/config.toml present")
112
+ context_path = _latest_context_path(root)
112
113
  if context_path.exists():
113
114
  import time
114
115
  age = time.time() - context_path.stat().st_mtime
@@ -216,6 +217,16 @@ def register(app: typer.Typer) -> None:
216
217
  if not _local_has_mcp and not _global_has_mcp:
217
218
  console.print(" [yellow]![/] MCP server not registered — mcp__agentpack__* tools unavailable")
218
219
 
220
+ # --- Release hygiene ---
221
+ console.print("\n[bold]Release hygiene[/]")
222
+ findings = _release_hygiene_findings(root)
223
+ if findings:
224
+ for finding in findings:
225
+ console.print(f" [yellow]![/] {finding}")
226
+ ok = False
227
+ else:
228
+ console.print(" [green]✓[/] no generated release-noise files staged or untracked")
229
+
219
230
  # --- Slash command ---
220
231
  console.print("\n[bold]Slash command (/agentpack)[/]")
221
232
  local_cmd = root / ".claude" / "commands" / "agentpack.md"
@@ -244,6 +255,23 @@ def _check_agent_file(root: Path, filename: str, agent: str) -> None:
244
255
  console.print(f" [dim]-[/] {filename} not present (optional)")
245
256
 
246
257
 
258
+ def _latest_context_path(root: Path) -> Path:
259
+ meta = load_pack_metadata(root)
260
+ if meta and meta.get("context_path"):
261
+ candidate = root / str(meta["context_path"])
262
+ if candidate.exists():
263
+ return candidate
264
+ for rel in (
265
+ ".agentpack/context.md",
266
+ ".agentpack/context.claude.md",
267
+ ".agent/skills/agentpack/SKILL.md",
268
+ ):
269
+ candidate = root / rel
270
+ if candidate.exists():
271
+ return candidate
272
+ return root / ".agentpack" / "context.md"
273
+
274
+
247
275
  def _source_checkout_warning(
248
276
  root: Path,
249
277
  package_file: Path,
@@ -268,6 +296,51 @@ def _source_checkout_warning(
268
296
  )
269
297
 
270
298
 
299
+ _RELEASE_NOISE_PREFIXES = (
300
+ ".agent/",
301
+ ".agentpack/",
302
+ ".claude/worktrees/",
303
+ ".codex/",
304
+ ".cursor/",
305
+ ".vscode/",
306
+ )
307
+ _RELEASE_NOISE_FILES = {".coverage"}
308
+
309
+
310
+ def _release_hygiene_findings(root: Path) -> list[str]:
311
+ """Flag local generated artifacts that should not be staged or released."""
312
+ try:
313
+ result = subprocess.run(
314
+ ["git", "status", "--short"],
315
+ cwd=root,
316
+ capture_output=True,
317
+ text=True,
318
+ timeout=5,
319
+ )
320
+ except (OSError, subprocess.TimeoutExpired):
321
+ return ["could not inspect git status for release hygiene"]
322
+ if result.returncode != 0:
323
+ return []
324
+
325
+ noisy: list[str] = []
326
+ for raw in result.stdout.splitlines():
327
+ if not raw.strip():
328
+ continue
329
+ status = raw[:2].strip() or "modified"
330
+ path = raw[3:].strip()
331
+ if " -> " in path:
332
+ path = path.rsplit(" -> ", 1)[1]
333
+ norm = path.replace("\\", "/")
334
+ if norm in _RELEASE_NOISE_FILES or any(norm.startswith(prefix) for prefix in _RELEASE_NOISE_PREFIXES):
335
+ noisy.append(f"{status} {norm}")
336
+
337
+ if not noisy:
338
+ return []
339
+ sample = ", ".join(noisy[:8])
340
+ extra = f", ... {len(noisy) - 8} more" if len(noisy) > 8 else ""
341
+ return [f"generated/local artifacts present: {sample}{extra}"]
342
+
343
+
271
344
  def _print_summary(ok: bool) -> None:
272
345
  console.print("")
273
346
  if ok:
@@ -10,6 +10,7 @@ from agentpack.core.context_pack import select_files
10
10
  from agentpack.commands._shared import console, _root
11
11
  from agentpack.commands.pack import _resolve_task
12
12
  from agentpack.core.config import load_config, ScoringWeights
13
+ from agentpack.analysis.ranking import extract_keyword_weights, generic_task_term_ratio, _GENERIC_TASK_TERMS
13
14
 
14
15
 
15
16
  def _resolve_signal_weight(reason: str, weights: ScoringWeights) -> float:
@@ -111,6 +112,50 @@ def _print_file_detail(
111
112
  console.print()
112
113
 
113
114
 
115
+ def _noise_report(task: str, plan: object) -> list[str]:
116
+ keyword_weights = extract_keyword_weights(task)
117
+ generic_terms = sorted(term for term in keyword_weights if term in _GENERIC_TASK_TERMS)
118
+ specific_terms = sorted(term for term in keyword_weights if term not in _GENERIC_TASK_TERMS)
119
+ selected = list(plan.selected) # type: ignore[attr-defined]
120
+ summary_count = sum(1 for sf in selected if sf.include_mode == "summary")
121
+ filename_count = sum(1 for sf in selected if "filename keyword match" in sf.reasons)
122
+ symbol_count = sum(1 for sf in selected if "symbol keyword match" in sf.reasons)
123
+ excluded = [r for r in plan.receipts if r.action == "excluded"] # type: ignore[attr-defined]
124
+ summary_cap = sum(1 for r in excluded if r.reason == "summary cap reached")
125
+ score_floor = sum(1 for r in excluded if r.reason == "summary score below floor")
126
+
127
+ lines = [
128
+ "## Pack noise report",
129
+ "",
130
+ f"- generic task ratio: {generic_task_term_ratio(task):.0%}",
131
+ f"- generic terms: {', '.join(generic_terms) if generic_terms else '(none)'}",
132
+ f"- specific terms: {', '.join(specific_terms) if specific_terms else '(none)'}",
133
+ f"- selected summaries: {summary_count}/{len(selected)}",
134
+ f"- filename-match selections: {filename_count}/{len(selected)}",
135
+ f"- symbol-match selections: {symbol_count}/{len(selected)}",
136
+ f"- excluded by summary cap: {summary_cap}",
137
+ f"- excluded by weak summary score: {score_floor}",
138
+ "",
139
+ "### Sharpen task wording",
140
+ "",
141
+ ]
142
+ if generic_terms:
143
+ lines.append("- Replace broad terms with subsystem, file, or symptom words.")
144
+ lines.append(f"- Broad terms driving matches: {', '.join(generic_terms[:8])}.")
145
+ else:
146
+ lines.append("- Task terms are already specific; inspect changed files or score weights next.")
147
+ if summary_count and selected and summary_count / len(selected) >= 0.7:
148
+ lines.append("- Try `--mode minimal` for edit work, or add exact module/file names.")
149
+ if filename_count and selected and filename_count / len(selected) >= 0.6:
150
+ lines.append("- Filename matches dominate; add behavior words that appear inside target files.")
151
+ return lines
152
+
153
+
154
+ def _print_noise_report(task: str, plan: object) -> None:
155
+ for line in _noise_report(task, plan):
156
+ console.print(line)
157
+
158
+
114
159
  def register(app: typer.Typer) -> None:
115
160
  @app.command()
116
161
  def explain(
@@ -120,6 +165,7 @@ def register(app: typer.Typer) -> None:
120
165
  since: Optional[str] = typer.Option(None, "--since", help="Git ref to compare against (e.g. HEAD~1, main)."),
121
166
  file: Optional[str] = typer.Option(None, "--file", help="Show detailed score breakdown for a specific file."),
122
167
  omitted: bool = typer.Option(False, "--omitted", is_flag=True, help="Show top-10 excluded files and why."),
168
+ why_noisy: bool = typer.Option(False, "--why-noisy", is_flag=True, help="Explain broad task terms and noisy selection signals."),
123
169
  ) -> None:
124
170
  """Explain which files would be selected and why, without writing a context file."""
125
171
  if mode not in ("minimal", "balanced", "deep"):
@@ -199,6 +245,12 @@ def register(app: typer.Typer) -> None:
199
245
  console.print()
200
246
  return
201
247
 
248
+ if why_noisy:
249
+ console.print(f"\n[bold]Task:[/] [cyan]{resolved_task}[/] [dim]mode={mode} budget={plan.budget:,}[/]\n")
250
+ _print_noise_report(resolved_task, plan)
251
+ console.print()
252
+ return
253
+
202
254
  console.print(f"\n[bold]Task:[/] [cyan]{resolved_task}[/] [dim]mode={mode} budget={plan.budget:,}[/]\n")
203
255
 
204
256
  console.print("[bold]Top selected files (ranked):[/]")
@@ -10,6 +10,54 @@ from agentpack.core.ignore import DEFAULT_AGENTIGNORE
10
10
  from agentpack.commands._shared import console, _root
11
11
  from agentpack.session.state import load_session, create_session, SESSION_FILE, TASK_FILE
12
12
 
13
+ _GITIGNORE_START = "# agentpack:start"
14
+ _GITIGNORE_END = "# agentpack:end"
15
+
16
+
17
+ def _repo_gitignore_block(share_cache: bool = False) -> str:
18
+ cache_line = "" if share_cache else ".agentpack/cache/\n"
19
+ return (
20
+ f"{_GITIGNORE_START}\n"
21
+ "# AgentPack generated context/cache (safe to ignore)\n"
22
+ f"{cache_line}"
23
+ ".agentpack/snapshots/\n"
24
+ ".agentpack/context*\n"
25
+ ".agentpack/metrics.jsonl\n"
26
+ ".agentpack/pack_metadata.json\n"
27
+ ".agentpack/activity.log\n"
28
+ ".agentpack/.gitignore\n"
29
+ ".agentpack/.mcp_reminded\n"
30
+ ".agentpack/session.json\n"
31
+ ".agentpack/task.md\n"
32
+ ".agentpack/benchmark_results.jsonl\n"
33
+ ".agent/skills/agentpack/\n"
34
+ f"{_GITIGNORE_END}\n"
35
+ )
36
+
37
+
38
+ def _patch_repo_gitignore(root, share_cache: bool = False) -> str:
39
+ gitignore = root / ".gitignore"
40
+ block = _repo_gitignore_block(share_cache)
41
+ if not gitignore.exists():
42
+ gitignore.write_text(block, encoding="utf-8")
43
+ return "created"
44
+
45
+ content = gitignore.read_text(encoding="utf-8")
46
+ start = content.find(_GITIGNORE_START)
47
+ end = content.find(_GITIGNORE_END)
48
+ if start != -1 and end != -1 and end >= start:
49
+ end += len(_GITIGNORE_END)
50
+ replacement = block.rstrip()
51
+ updated = content[:start].rstrip() + "\n\n" + replacement + "\n" + content[end:].lstrip("\n")
52
+ if updated == content:
53
+ return "unchanged"
54
+ gitignore.write_text(updated, encoding="utf-8")
55
+ return "updated"
56
+
57
+ prefix = content.rstrip() + "\n\n" if content.strip() else ""
58
+ gitignore.write_text(prefix + block, encoding="utf-8")
59
+ return "updated"
60
+
13
61
 
14
62
  def register(app: typer.Typer) -> None:
15
63
  @app.command()
@@ -48,6 +96,14 @@ def register(app: typer.Typer) -> None:
48
96
  else:
49
97
  console.print("[dim]Skipped[/] .agentpack/.gitignore (exists)")
50
98
 
99
+ gitignore_action = _patch_repo_gitignore(root, share_cache=share_cache)
100
+ if gitignore_action == "created":
101
+ console.print("[green]Created[/] .gitignore [dim](AgentPack generated artifacts ignored)[/]")
102
+ elif gitignore_action == "updated":
103
+ console.print("[green]Updated[/] .gitignore [dim](AgentPack generated artifacts ignored)[/]")
104
+ else:
105
+ console.print("[dim]Skipped[/] .gitignore (AgentPack block unchanged)")
106
+
51
107
  config_path_file = agentpack_dir / "config.toml"
52
108
  if not config_path_file.exists() or force:
53
109
  cfg = DEFAULT_CONFIG.model_copy(deep=True)
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import time
4
+ from pathlib import Path
4
5
  from typing import Optional
5
6
 
6
7
  import typer
@@ -12,7 +13,8 @@ from rich import box
12
13
  from agentpack.core import git
13
14
  from agentpack.core.ignore import SENSITIVE_PATTERNS
14
15
  from agentpack.application.pack_service import PackRequest, PackService, PackResult
15
- from agentpack.commands._shared import console, _root
16
+ from agentpack.commands._shared import console, _root, _file_hash, _now_iso
17
+ from agentpack.session.state import TASK_FILE, load_session, save_session, log_activity
16
18
 
17
19
 
18
20
  def register(app: typer.Typer) -> None:
@@ -50,6 +52,7 @@ def register(app: typer.Typer) -> None:
50
52
  refresh=refresh,
51
53
  task_source=task_source,
52
54
  ))
55
+ _mark_session_refreshed(_root(), result)
53
56
  _print_pack_summary(result)
54
57
 
55
58
 
@@ -177,6 +180,20 @@ def _print_pack_summary(result: PackResult) -> None:
177
180
  console.print()
178
181
 
179
182
 
183
+ def _mark_session_refreshed(root: Path, result: PackResult) -> None:
184
+ state = load_session(root)
185
+ if state is None or not state.active:
186
+ return
187
+ freshness = result.pack.freshness or {}
188
+ state.last_refresh_at = freshness.get("generated_at") or _now_iso()
189
+ state.refresh_count += 1
190
+ state.last_task_hash = _file_hash(root / TASK_FILE)
191
+ state.last_git_hash = freshness.get("snapshot_root_hash", "")
192
+ state.last_resolved_agent = getattr(result.pack, "agent", state.last_resolved_agent)
193
+ save_session(root, state)
194
+ log_activity(root, f"pack refresh — {len(result.pack.selected_files)} files, {result.packed_tokens:,} tokens")
195
+
196
+
180
197
  def _pack_diagnostics(result: PackResult) -> list[str]:
181
198
  selected = result.pack.selected_files
182
199
  receipts = result.pack.receipts
@@ -186,6 +203,9 @@ def _pack_diagnostics(result: PackResult) -> list[str]:
186
203
  symbol_matches = sum(1 for sf in selected if "symbol keyword match" in sf.reasons)
187
204
  score_floor_excluded = sum(1 for r in receipts if r.reason == "summary score below floor")
188
205
  summary_cap_excluded = sum(1 for r in receipts if r.reason == "summary cap reached")
206
+ changed_set = set(result.changed_files)
207
+ top_changed = sum(1 for sf in selected[:10] if sf.path in changed_set)
208
+ strong_live_signal = bool(changed_set) and top_changed >= min(len(changed_set), 5)
189
209
 
190
210
  task_words = [
191
211
  part for part in result.pack.task.replace("_", " ").replace("-", " ").split()
@@ -195,9 +215,9 @@ def _pack_diagnostics(result: PackResult) -> list[str]:
195
215
  diagnostics.append("Task is very short; add subsystem, file, or symptom words for better precision.")
196
216
  if not result.changed_files:
197
217
  diagnostics.append("No changed files detected; pack relies mostly on task keywords and cached summaries.")
198
- if selected and filename_matches / len(selected) >= 0.6:
218
+ if selected and not strong_live_signal and filename_matches / len(selected) >= 0.6:
199
219
  diagnostics.append("Most selected files matched by filename; task terms may be broad.")
200
- if selected and summary_count / len(selected) >= 0.7:
220
+ if selected and not strong_live_signal and summary_count / len(selected) >= 0.7:
201
221
  diagnostics.append("Pack is mostly summaries; use minimal mode or a more specific task for edit work.")
202
222
  if symbol_matches > 25:
203
223
  diagnostics.append(f"Many symbol matches selected ({symbol_matches}); inspect repeated task terms with explain.")
@@ -232,6 +252,7 @@ def _pack_watch(
232
252
  root=root, agent=agent, task=task, mode=mode, budget=budget,
233
253
  since=since, refresh=False, task_source="watch",
234
254
  ))
255
+ _mark_session_refreshed(root, result)
235
256
  _print_pack_summary(result)
236
257
 
237
258
  _run_pack()
@@ -6,6 +6,7 @@ from rich.table import Table
6
6
  from agentpack.core.config import load_config
7
7
  from agentpack.core.ignore import load_spec
8
8
  from agentpack.core.scanner import scan
9
+ from agentpack.application.pack_service import AdapterRegistry
9
10
  from agentpack.commands._shared import console, _root
10
11
 
11
12
 
@@ -18,7 +19,12 @@ def register(app: typer.Typer) -> None:
18
19
  ignore_spec = load_spec(root / cfg.project.ignore_file)
19
20
 
20
21
  console.print("[bold]Scanning repository...[/]")
21
- scan_result = scan(root, ignore_spec, cfg.context.max_file_tokens)
22
+ scan_result = scan(
23
+ root,
24
+ ignore_spec,
25
+ cfg.context.max_file_tokens,
26
+ always_skip_paths=AdapterRegistry.generated_output_paths(root, cfg),
27
+ )
22
28
 
23
29
  total = len(scan_result.all_files)
24
30
  ignored = len(scan_result.ignored) + len(scan_result.binary)
@@ -1,17 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ from datetime import datetime, timezone
4
5
  from pathlib import Path
5
6
 
6
7
  import typer
7
8
  from rich.table import Table
8
9
  from rich import box
10
+ from rich.panel import Panel
9
11
 
12
+ from agentpack.core import git
10
13
  from agentpack.core.config import load_config
11
14
  from agentpack.core.ignore import load_spec
12
15
  from agentpack.core.scanner import scan
16
+ from agentpack.core.snapshot import build_snapshot
13
17
  from agentpack.core.context_pack import load_pack_metadata
18
+ from agentpack.application.pack_service import AdapterRegistry
14
19
  from agentpack.commands._shared import console, _root
20
+ from agentpack.session.state import SessionState
15
21
 
16
22
 
17
23
  def register(app: typer.Typer) -> None:
@@ -22,7 +28,12 @@ def register(app: typer.Typer) -> None:
22
28
  cfg = load_config(root)
23
29
  ignore_spec = load_spec(root / cfg.project.ignore_file)
24
30
 
25
- scan_result = scan(root, ignore_spec, cfg.context.max_file_tokens)
31
+ scan_result = scan(
32
+ root,
33
+ ignore_spec,
34
+ cfg.context.max_file_tokens,
35
+ always_skip_paths=AdapterRegistry.generated_output_paths(root, cfg),
36
+ )
26
37
  meta = load_pack_metadata(root)
27
38
 
28
39
  raw = sum(f.estimated_tokens for f in scan_result.all_files)
@@ -55,6 +66,8 @@ def register(app: typer.Typer) -> None:
55
66
  sess_tbl.add_column(style="bold")
56
67
  sess_tbl.add_row("active", "[green]yes[/]" if session.active else "[red]no[/]")
57
68
  sess_tbl.add_row("agent", session.agent)
69
+ if session.last_resolved_agent and session.last_resolved_agent != session.agent:
70
+ sess_tbl.add_row("last pack agent", session.last_resolved_agent)
58
71
  sess_tbl.add_row("mode", session.mode)
59
72
  if session.started_at:
60
73
  sess_tbl.add_row("started", session.started_at[:19].replace("T", " "))
@@ -75,11 +88,24 @@ def register(app: typer.Typer) -> None:
75
88
  except Exception:
76
89
  pass
77
90
 
91
+ context_path_obj = None
78
92
  if meta:
79
93
  context_path_obj = root / meta.get("context_path", "")
80
- if context_path_obj.exists():
94
+ top_files = _top_files_from_metadata(meta)
95
+ if not top_files and context_path_obj.exists():
81
96
  top_files = _parse_top_files(context_path_obj)
82
97
 
98
+ freshness_diagnostics = _freshness_diagnostics(
99
+ root=root,
100
+ meta=meta,
101
+ session=session,
102
+ current_root_hash=build_snapshot(scan_result.packable)["root_hash"],
103
+ context_path=context_path_obj,
104
+ )
105
+ if freshness_diagnostics:
106
+ console.print()
107
+ console.print(_advice_panel("Freshness advice", freshness_diagnostics))
108
+
83
109
  token_by_path = {f.path: f.estimated_tokens for f in scan_result.packable}
84
110
  top_estimate = sum(token_by_path.get(path, 0) for path, _mode, _why in top_files[:20])
85
111
  if top_estimate <= 0:
@@ -116,6 +142,11 @@ def register(app: typer.Typer) -> None:
116
142
 
117
143
  # --- Selection accuracy (last 10 runs) ---
118
144
  accuracy_rows = _load_accuracy_rows(metrics_path, n=10)
145
+ noise_diagnostics = _noise_diagnostics(top_files, accuracy_rows)
146
+ if noise_diagnostics:
147
+ console.print()
148
+ console.print(_advice_panel("Pack quality advice", noise_diagnostics))
149
+
119
150
  if accuracy_rows:
120
151
  avg_recall = sum(r["selection_recall"] for r in accuracy_rows) / len(accuracy_rows)
121
152
  avg_precision = sum(r["selection_precision"] for r in accuracy_rows) / len(accuracy_rows)
@@ -174,6 +205,126 @@ def _load_accuracy_rows(metrics_path: Path, n: int = 10) -> list[dict]:
174
205
  return []
175
206
 
176
207
 
208
+ def _task_md_body(root: Path) -> str | None:
209
+ path = root / ".agentpack" / "task.md"
210
+ try:
211
+ content = path.read_text(encoding="utf-8").strip()
212
+ except OSError:
213
+ return None
214
+ lines = [line for line in content.splitlines() if line.strip() and not line.startswith("#")]
215
+ body = lines[0].strip() if lines else ""
216
+ if body and "Write or update the current coding task here." not in body:
217
+ return body
218
+ return None
219
+
220
+
221
+ def _parse_iso(value: str | None) -> datetime | None:
222
+ if not value:
223
+ return None
224
+ try:
225
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
226
+ return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
227
+ except ValueError:
228
+ return None
229
+
230
+
231
+ def _freshness_diagnostics(
232
+ *,
233
+ root: Path,
234
+ meta: dict | None,
235
+ session: SessionState | None,
236
+ current_root_hash: str,
237
+ context_path: Path | None,
238
+ ) -> list[str]:
239
+ if not meta:
240
+ return ["No pack metadata found; run `agentpack pack --task auto`."]
241
+
242
+ diagnostics: list[str] = []
243
+ for warning in meta.get("freshness_warnings") or []:
244
+ diagnostics.append(str(warning))
245
+
246
+ task_md = _task_md_body(root)
247
+ if task_md and task_md != meta.get("task"):
248
+ diagnostics.append(".agentpack/task.md differs from the latest packed task.")
249
+
250
+ if meta.get("snapshot_root_hash") and meta.get("snapshot_root_hash") != current_root_hash:
251
+ diagnostics.append("Files changed since the latest pack; refresh before trusting top included files.")
252
+
253
+ if git.is_git_repo(root):
254
+ packed_sha = meta.get("git_sha") or (meta.get("freshness") or {}).get("git_sha")
255
+ current_sha = git.current_sha(root)
256
+ if packed_sha and current_sha and packed_sha != current_sha:
257
+ diagnostics.append("Git HEAD changed since the latest pack.")
258
+
259
+ if context_path is not None and not context_path.exists():
260
+ diagnostics.append(f"Recorded context path is missing: {context_path.relative_to(root)}.")
261
+
262
+ if session and session.active:
263
+ packed_at = _parse_iso(meta.get("generated_at"))
264
+ refreshed_at = _parse_iso(session.last_refresh_at)
265
+ if not session.last_refresh_at:
266
+ diagnostics.append("Session is active but has no last refresh timestamp.")
267
+ elif packed_at and refreshed_at and refreshed_at < packed_at:
268
+ diagnostics.append("Session last refresh timestamp is older than latest pack metadata.")
269
+
270
+ return diagnostics[:5]
271
+
272
+
273
+ def _noise_diagnostics(
274
+ top_files: list[tuple[str, str, str]],
275
+ accuracy_rows: list[dict],
276
+ ) -> list[str]:
277
+ diagnostics: list[str] = []
278
+ if top_files:
279
+ summary_count = sum(1 for _path, mode, _why in top_files if mode == "summary")
280
+ filename_matches = sum(1 for _path, _mode, why in top_files if "filename keyword match" in why)
281
+ if summary_count / len(top_files) >= 0.7:
282
+ diagnostics.append("Latest pack is mostly summaries; use minimal mode or a narrower task for edit work.")
283
+ if filename_matches / len(top_files) >= 0.6:
284
+ diagnostics.append("Top files mostly matched by filename; task terms may be broad.")
285
+
286
+ if accuracy_rows:
287
+ avg_precision = sum(r["selection_precision"] for r in accuracy_rows) / len(accuracy_rows)
288
+ token_rows = [r for r in accuracy_rows if "selection_token_precision" in r]
289
+ avg_token_precision = (
290
+ sum(r["selection_token_precision"] for r in token_rows) / len(token_rows)
291
+ if token_rows else None
292
+ )
293
+ summary_rows = [r for r in accuracy_rows if "selection_token_precision_summary" in r]
294
+ avg_summary_precision = (
295
+ sum(r["selection_token_precision_summary"] for r in summary_rows) / len(summary_rows)
296
+ if summary_rows else None
297
+ )
298
+ if avg_precision < 0.05:
299
+ diagnostics.append("Selection file precision is very low; many selected files were not later changed.")
300
+ if avg_token_precision is not None and avg_token_precision < 0.2:
301
+ diagnostics.append("Token precision is low; most packed tokens became noise in recent runs.")
302
+ if avg_summary_precision == 0:
303
+ diagnostics.append("Summary token precision is 0%; summary context has not matched later edits.")
304
+ return diagnostics[:5]
305
+
306
+
307
+ def _top_files_from_metadata(meta: dict) -> list[tuple[str, str, str]]:
308
+ files = meta.get("selected_files_meta") or []
309
+ if not isinstance(files, list):
310
+ return []
311
+ result: list[tuple[str, str, str]] = []
312
+ for item in files:
313
+ if not isinstance(item, dict):
314
+ continue
315
+ path = item.get("path")
316
+ mode = item.get("mode")
317
+ why = item.get("why") or ""
318
+ if isinstance(path, str) and isinstance(mode, str):
319
+ result.append((path, mode, str(why)))
320
+ return result
321
+
322
+
323
+ def _advice_panel(title: str, diagnostics: list[str]) -> Panel:
324
+ body = "\n".join(f" [cyan]i[/] {line}" for line in diagnostics)
325
+ return Panel(body, title=f"[bold cyan]{title}[/]", border_style="cyan", padding=(0, 1))
326
+
327
+
177
328
  def _parse_top_files(context_path: Path) -> list[tuple[str, str, str]]:
178
329
  """Parse top selected files from context.md. Returns list of (path, mode, why)."""
179
330
  results: list[tuple[str, str, str]] = []
@@ -7,6 +7,7 @@ from agentpack.core.ignore import load_spec
7
7
  from agentpack.core.scanner import scan
8
8
  from agentpack.core.snapshot import build_snapshot
9
9
  from agentpack.core.context_pack import load_pack_metadata
10
+ from agentpack.application.pack_service import AdapterRegistry
10
11
  from agentpack.commands._shared import console, _root
11
12
 
12
13
 
@@ -23,7 +24,12 @@ def register(app: typer.Typer) -> None:
23
24
  console.print("[yellow]No context pack found. Run agentpack pack to generate one.[/]")
24
25
  raise typer.Exit(1)
25
26
 
26
- scan_result = scan(root, ignore_spec, cfg.context.max_file_tokens)
27
+ scan_result = scan(
28
+ root,
29
+ ignore_spec,
30
+ cfg.context.max_file_tokens,
31
+ always_skip_paths=AdapterRegistry.generated_output_paths(root, cfg),
32
+ )
27
33
  current = build_snapshot(scan_result.packable)
28
34
 
29
35
  if current["root_hash"] == meta.get("snapshot_root_hash"):
@@ -5,6 +5,7 @@ import typer
5
5
  from agentpack.core.config import load_config
6
6
  from agentpack.core.ignore import load_spec
7
7
  from agentpack.core.scanner import scan
8
+ from agentpack.application.pack_service import AdapterRegistry
8
9
  from agentpack.summaries.base import get_or_build_summary
9
10
  from agentpack.commands._shared import console, _root
10
11
 
@@ -21,7 +22,12 @@ def register(app: typer.Typer) -> None:
21
22
 
22
23
  console.print("[bold]Building offline summaries...[/]")
23
24
 
24
- scan_result = scan(root, ignore_spec, cfg.context.max_file_tokens)
25
+ scan_result = scan(
26
+ root,
27
+ ignore_spec,
28
+ cfg.context.max_file_tokens,
29
+ always_skip_paths=AdapterRegistry.generated_output_paths(root, cfg),
30
+ )
25
31
  active = scan_result.packable
26
32
 
27
33
  if refresh:
@@ -118,6 +118,7 @@ def _run_refresh(root: Path, agent: str, mode: str, budget: int) -> None:
118
118
  state.last_refresh_at = _now_iso()
119
119
  state.refresh_count += 1
120
120
  state.last_task_hash = _file_hash(root / TASK_FILE)
121
+ state.last_resolved_agent = agent
121
122
  save_session(root, state)
122
123
  log_activity(root, f"watch refresh — {result['files']} files, {result['tokens']:,} tokens")
123
124
  else:
@@ -57,6 +57,7 @@ def save_pack_metadata(
57
57
  token_estimate: int = 0,
58
58
  freshness: dict[str, Any] | None = None,
59
59
  freshness_warnings: list[str] | None = None,
60
+ selected_files: list[dict[str, Any]] | None = None,
60
61
  ) -> None:
61
62
  generated_at = (
62
63
  freshness.get("generated_at")
@@ -72,6 +73,7 @@ def save_pack_metadata(
72
73
  "mode": mode,
73
74
  "budget": budget,
74
75
  "token_estimate": token_estimate,
76
+ "selected_files_meta": selected_files or [],
75
77
  "freshness": freshness or {},
76
78
  "freshness_warnings": freshness_warnings or [],
77
79
  }
@@ -91,6 +91,7 @@ def scan(
91
91
  previous_snapshot: dict | None = None,
92
92
  include_globs: list[str] | None = None,
93
93
  exclude_globs: list[str] | None = None,
94
+ always_skip_paths: set[str] | None = None,
94
95
  ) -> ScanResult:
95
96
  packable: list[FileInfo] = []
96
97
  ignored: list[FileInfo] = []
@@ -98,6 +99,7 @@ def scan(
98
99
 
99
100
  prev_files: dict[str, dict] = (previous_snapshot or {}).get("files", {})
100
101
  inc_spec, exc_spec = _build_glob_specs(include_globs or [], exclude_globs or [])
102
+ generated_paths = {p.replace("\\", "/") for p in (always_skip_paths or set())}
101
103
 
102
104
  for abs_path in root.rglob("*"):
103
105
  if not abs_path.is_file():
@@ -110,6 +112,8 @@ def scan(
110
112
  continue
111
113
 
112
114
  rel_str = str(rel)
115
+ if rel_str.replace("\\", "/") in generated_paths:
116
+ continue
113
117
 
114
118
  if inc_spec is not None and not inc_spec.match_file(rel_str):
115
119
  ignored.append(FileInfo(
@@ -34,6 +34,7 @@ class SessionState:
34
34
  last_refresh_at: Optional[str] = None
35
35
  last_task_hash: str = ""
36
36
  last_git_hash: str = ""
37
+ last_resolved_agent: str = ""
37
38
  refresh_count: int = 0
38
39
 
39
40
 
@@ -53,6 +54,7 @@ def load_session(root: Path) -> Optional[SessionState]:
53
54
  last_refresh_at=data.get("last_refresh_at"),
54
55
  last_task_hash=data.get("last_task_hash", ""),
55
56
  last_git_hash=data.get("last_git_hash", ""),
57
+ last_resolved_agent=data.get("last_resolved_agent", ""),
56
58
  refresh_count=data.get("refresh_count", 0),
57
59
  )
58
60
  except FileNotFoundError:
@@ -1,21 +0,0 @@
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/cache/
14
- .agentpack/snapshots/
15
- .agentpack/context.*
16
-
17
- .pytest_cache/
18
- .mypy_cache/
19
- .ruff_cache/
20
-
21
- *.dist-info/
File without changes