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.
- agentpack_cli-0.1.25/.gitignore +33 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/PKG-INFO +6 -2
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/README.md +5 -1
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/pyproject.toml +1 -1
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/__init__.py +1 -1
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/application/pack_service.py +51 -2
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/claude_cmd.py +1 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/diff.py +7 -1
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/doctor.py +74 -1
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/explain.py +52 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/init.py +56 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/pack.py +24 -3
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/scan.py +7 -1
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/stats.py +153 -2
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/status.py +7 -1
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/summarize.py +7 -1
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/watch.py +1 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/context_pack.py +2 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/scanner.py +4 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/session/state.py +2 -0
- agentpack_cli-0.1.24/.gitignore +0 -21
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/LICENSE +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/__init__.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/antigravity.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/base.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/claude.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/codex.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/cursor.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/detect.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/generic.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/adapters/windsurf.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/__init__.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/dependency_graph.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/go_imports.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/java_imports.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/js_ts_imports.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/python_imports.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/ranking.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/rust_imports.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/symbols.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/analysis/tests.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/application/__init__.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/cli.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/__init__.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/_shared.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/benchmark.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/hook_cmd.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/install.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/mcp_cmd.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/monitor.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/commands/quickstart.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/__init__.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/bootstrap.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/cache.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/config.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/diff.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/git.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/git_hooks.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/global_install.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/ignore.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/merkle.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/models.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/redactor.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/snapshot.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/token_estimator.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/core/vscode_tasks.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/data/agentpack.md +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/installers/__init__.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/installers/antigravity.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/installers/claude.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/installers/codex.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/installers/cursor.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/installers/windsurf.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/integrations/__init__.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/integrations/git_hooks.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/integrations/global_install.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/integrations/vscode_tasks.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/mcp_server.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/renderers/__init__.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/renderers/compact.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/renderers/markdown.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/renderers/receipts.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/session/__init__.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/summaries/__init__.py +0 -0
- {agentpack_cli-0.1.24 → agentpack_cli-0.1.25}/src/agentpack/summaries/base.py +0 -0
- {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.
|
|
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
|
[](https://opensource.org/licenses/MIT)
|
|
45
45
|
[](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml)
|
|
46
46
|
|
|
47
|
-
> **Status: alpha (v0.1.
|
|
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
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml)
|
|
7
7
|
|
|
8
|
-
> **Status: alpha (v0.1.
|
|
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
|
---
|
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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:
|
agentpack_cli-0.1.24/.gitignore
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|