@inneranimalmedia/agentsam-sdk 2.0.0 → 2.2.0

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 (61) hide show
  1. package/README.md +28 -15
  2. package/docs/CAPABILITIES.md +93 -0
  3. package/docs/DEPLOY_RECEIPTS.md +83 -0
  4. package/docs/RECON.md +165 -0
  5. package/docs/RELEASES.md +4 -3
  6. package/docs/portable-knowledge.md +4 -4
  7. package/docs/sdk-2.0-release.md +19 -21
  8. package/package.json +8 -1
  9. package/packages/identity/package.json +1 -1
  10. package/packages/identity/src/frontend/auth-portal/README.md +1 -1
  11. package/packages/identity/src/index.js +2 -0
  12. package/protocol/capabilities/capability-manifest.schema.json +36 -0
  13. package/protocol/capabilities/manifest.json +179 -0
  14. package/protocol/capabilities/repository-audit-input.schema.json +14 -0
  15. package/protocol/capabilities/repository-audit.schema.json +30 -0
  16. package/protocol/capabilities/repository-snapshot-input.schema.json +11 -0
  17. package/protocol/capabilities/repository-snapshot.schema.json +20 -0
  18. package/protocol/knowledge/chunk.schema.json +15 -73
  19. package/protocol/knowledge/document.schema.json +10 -48
  20. package/protocol/knowledge/index-config.schema.json +2 -2
  21. package/protocol/knowledge/repository.schema.json +11 -52
  22. package/protocol/knowledge/retrieval-query.schema.json +13 -63
  23. package/protocol/knowledge/source.schema.json +9 -43
  24. package/protocol/presets/catalog.json +48 -0
  25. package/protocol/recon/README.md +19 -0
  26. package/protocol/recon/finding-report.schema.json +46 -0
  27. package/protocol/recon/task-packet.schema.json +79 -0
  28. package/python/agentsam_sdk/knowledge/models.py +19 -7
  29. package/python/agentsam_sdk/repository/__main__.py +2 -2
  30. package/python/agentsam_sdk/repository/recon/__init__.py +28 -0
  31. package/python/agentsam_sdk/repository/recon/__main__.py +3 -0
  32. package/python/agentsam_sdk/repository/recon/cli.py +178 -0
  33. package/python/agentsam_sdk/repository/recon/packet.py +294 -0
  34. package/python/agentsam_sdk/repository/recon/validate.py +76 -0
  35. package/python/tests/test_knowledge_models.py +6 -2
  36. package/python/tests/test_recon.py +257 -0
  37. package/src/agent/capability-adapter.js +50 -0
  38. package/src/agent/index.js +2 -0
  39. package/src/agent/repository-audit.js +188 -0
  40. package/src/capabilities/index.js +7 -0
  41. package/src/capabilities/manifest.js +22 -0
  42. package/src/capabilities/repository-snapshot.js +180 -0
  43. package/src/cli.js +68 -8
  44. package/src/commands/deploy-receipt.js +129 -0
  45. package/src/commands/deploy.js +0 -1
  46. package/src/commands/knowledge.js +5 -6
  47. package/src/commands/product.js +119 -0
  48. package/src/commands/recon.js +71 -0
  49. package/src/index.js +17 -0
  50. package/src/knowledge/config.js +12 -6
  51. package/src/knowledge/contracts.js +1 -1
  52. package/src/knowledge/engine.js +1 -1
  53. package/src/knowledge/service/server.js +2 -2
  54. package/src/lib/deploy-receipt/index.js +246 -0
  55. package/src/lib/git-context.js +3 -1
  56. package/src/presets/index.js +20 -0
  57. package/src/repository/index.js +4 -0
  58. package/test/agent-capabilities.test.mjs +67 -0
  59. package/test/capabilities.test.mjs +84 -0
  60. package/test/deploy-receipt.test.mjs +91 -0
  61. package/test/portable-context.test.mjs +11 -1
@@ -4,49 +4,15 @@
4
4
  "type": "object",
5
5
  "additionalProperties": false,
6
6
  "title": "Source",
7
- "required": [
8
- "source_id",
9
- "source_type",
10
- "workspace_id",
11
- "uri"
12
- ],
7
+ "required": ["source_id", "source_type", "uri"],
13
8
  "properties": {
14
- "source_id": {
15
- "type": "string",
16
- "minLength": 1
17
- },
18
- "source_type": {
19
- "type": "string",
20
- "minLength": 1
21
- },
22
- "workspace_id": {
23
- "type": "string",
24
- "minLength": 1
25
- },
26
- "uri": {
27
- "type": "string",
28
- "minLength": 1
29
- },
30
- "repository_full_name": {
31
- "type": [
32
- "string",
33
- "null"
34
- ]
35
- },
36
- "ref": {
37
- "type": [
38
- "string",
39
- "null"
40
- ]
41
- },
42
- "commit_sha": {
43
- "type": [
44
- "string",
45
- "null"
46
- ]
47
- },
48
- "metadata": {
49
- "type": "object"
50
- }
9
+ "source_id": { "type": "string", "minLength": 1 },
10
+ "source_type": { "type": "string", "minLength": 1 },
11
+ "workspace_id": { "type": "string", "minLength": 1, "deprecated": true, "description": "Legacy compatibility metadata only." },
12
+ "uri": { "type": "string", "minLength": 1 },
13
+ "repository_full_name": { "type": ["string", "null"] },
14
+ "ref": { "type": ["string", "null"] },
15
+ "commit_sha": { "type": ["string", "null"] },
16
+ "metadata": { "type": "object" }
51
17
  }
52
18
  }
@@ -0,0 +1,48 @@
1
+ {
2
+ "schema_version": 1,
3
+ "presets": {
4
+ "fullstack": {
5
+ "id": "fullstack",
6
+ "version": 1,
7
+ "lane": "fullstack",
8
+ "description": "Local-first application with AgentSam runtime, repository primitives, knowledge, and delivery hooks.",
9
+ "features": ["agent", "knowledge"],
10
+ "capabilities": ["scaffold.create", "repository.snapshot", "knowledge.index", "knowledge.search", "security.scan", "deploy.receipt"],
11
+ "defaults": { "database": "sqlite", "assets": "local", "deploy": "local" }
12
+ },
13
+ "cms": {
14
+ "id": "cms",
15
+ "version": 1,
16
+ "lane": "cms",
17
+ "description": "Content-oriented application starting from local CMS primitives with explicit auth/deploy opt-ins.",
18
+ "features": ["cms", "knowledge"],
19
+ "capabilities": ["scaffold.create", "repository.snapshot", "knowledge.index", "knowledge.search", "site.scrape", "deploy.receipt"],
20
+ "defaults": { "database": "sqlite", "assets": "local", "deploy": "local" }
21
+ },
22
+ "prototype": {
23
+ "id": "prototype",
24
+ "version": 1,
25
+ "lane": "fullstack",
26
+ "description": "Small local prototype with the minimum deterministic AgentSam runtime.",
27
+ "features": ["agent"],
28
+ "capabilities": ["scaffold.create", "repository.snapshot", "security.scan"],
29
+ "defaults": { "database": "sqlite", "assets": "local", "deploy": "local" }
30
+ },
31
+ "data": {
32
+ "id": "data",
33
+ "version": 1,
34
+ "lane": "data",
35
+ "description": "Data-oriented local application with repository knowledge and explicit production storage graduation.",
36
+ "features": ["knowledge"],
37
+ "capabilities": ["scaffold.create", "repository.snapshot", "knowledge.index", "knowledge.search", "security.scan"],
38
+ "defaults": { "database": "sqlite", "assets": "local", "deploy": "local" }
39
+ }
40
+ },
41
+ "addons": {
42
+ "agent": { "id": "agent", "capabilities": ["scaffold.create"], "description": "AgentSam local runtime selection." },
43
+ "auth": { "id": "auth", "capabilities": ["identity.init"], "description": "Identity package selection; use `agentsam identity init` when a standalone auth app scaffold is required." },
44
+ "cms": { "id": "cms", "capabilities": ["site.scrape"], "description": "CMS/content feature selection." },
45
+ "knowledge": { "id": "knowledge", "capabilities": ["knowledge.index", "knowledge.search"], "description": "Portable repository indexing and retrieval." },
46
+ "deploy-cloudflare": { "id": "deploy-cloudflare", "capabilities": ["deploy.receipt"], "description": "Select Cloudflare as the intended deploy target; provisioning remains explicit at deploy time." }
47
+ }
48
+ }
@@ -0,0 +1,19 @@
1
+ # AgentSam Recon Protocol (aka "MiniCodeScout")
2
+
3
+ Provider-neutral contracts for delegating bounded read-only investigation to a cheap or
4
+ local model (Ollama/Qwen-class, mini-tier hosted models, etc.) without giving it branch,
5
+ Git, or write authority. See [docs/RECON.md](../../docs/RECON.md) for the design rationale
6
+ and current status — the delegation step itself is currently benched in favor of plain
7
+ deterministic search; these contracts are the reusable substrate for when it isn't.
8
+
9
+ `task-packet.schema.json` is the only input a recon worker receives. It is built by a
10
+ deterministic controller (`repository.intelligence`, `rg`, AST lookups) — never by the
11
+ worker itself, and never contains a workspace/tenant ID as the task handle.
12
+
13
+ `finding-report.schema.json` is the only output a recon worker may return. A worker that
14
+ cannot answer from its supplied slices must return `status: "needs_context"`, not a guess.
15
+
16
+ These schemas intentionally do not define a code-mutation contract. Recon workers observe;
17
+ they do not write, `git commit`, migrate, or merge. A future `bounded-mutation` protocol
18
+ (scoped to specific, proven-safe transforms) is a separate contract, not an extension of
19
+ this one — see ownership rule 6 in `protocol/README.md`.
@@ -0,0 +1,46 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.inneranimalmedia.com/agentsam/recon/finding-report.schema.json",
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "title": "ReconFindingReport",
7
+ "description": "The only artifact a bounded recon worker may produce. It never edits files, never touches Git, and never owns a branch.",
8
+ "required": ["schema_version", "task_id", "status"],
9
+ "oneOf": [
10
+ { "required": ["status"], "properties": { "status": { "const": "needs_context" } } },
11
+ { "required": ["status"], "properties": { "status": { "const": "answered" } } }
12
+ ],
13
+ "properties": {
14
+ "schema_version": { "const": 1 },
15
+ "task_id": { "type": "string", "minLength": 1 },
16
+ "status": { "enum": ["answered", "needs_context"] },
17
+ "summary": { "type": "string" },
18
+ "findings": {
19
+ "type": "array",
20
+ "items": {
21
+ "type": "object",
22
+ "additionalProperties": false,
23
+ "required": ["severity", "file", "finding"],
24
+ "properties": {
25
+ "severity": { "enum": ["low", "medium", "high"] },
26
+ "file": { "type": "string" },
27
+ "lines": { "type": "string" },
28
+ "finding": { "type": "string" },
29
+ "evidence": { "type": "string" },
30
+ "recommended_action": { "type": "string" }
31
+ }
32
+ }
33
+ },
34
+ "affected_files": { "type": "array", "items": { "type": "string" } },
35
+ "unknowns": { "type": "array", "items": { "type": "string" } },
36
+ "missing": {
37
+ "type": "array",
38
+ "items": { "type": "string" },
39
+ "description": "Required when status is needs_context: what evidence was missing."
40
+ },
41
+ "reason": {
42
+ "type": "string",
43
+ "description": "Required when status is needs_context: why the supplied slices were insufficient."
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,79 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.inneranimalmedia.com/agentsam/recon/task-packet.schema.json",
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "title": "ReconTaskPacket",
7
+ "description": "A bounded assignment handed to a cheap/local recon worker. Produced by a deterministic controller (rg/AST/repository.intelligence), never by the worker itself.",
8
+ "required": [
9
+ "schema_version",
10
+ "task_id",
11
+ "repo_root",
12
+ "base_sha",
13
+ "question",
14
+ "slices",
15
+ "ceilings"
16
+ ],
17
+ "properties": {
18
+ "schema_version": { "const": 1 },
19
+ "task_id": {
20
+ "type": "string",
21
+ "minLength": 1,
22
+ "description": "Caller-supplied identifier. Never a workspace/tenant ID — this is a task handle only."
23
+ },
24
+ "repo_root": { "type": "string", "minLength": 1 },
25
+ "base_sha": { "type": "string", "minLength": 1 },
26
+ "question": {
27
+ "type": "string",
28
+ "minLength": 1,
29
+ "description": "Exactly one bounded question. Not 'audit the subsystem'."
30
+ },
31
+ "slices": {
32
+ "type": "array",
33
+ "minItems": 1,
34
+ "maxItems": 5,
35
+ "description": "Exact file/line slices the worker may read. The worker must not open anything outside this list.",
36
+ "items": {
37
+ "type": "object",
38
+ "additionalProperties": false,
39
+ "required": ["path"],
40
+ "properties": {
41
+ "path": { "type": "string", "minLength": 1 },
42
+ "start_line": { "type": "integer", "minimum": 1 },
43
+ "end_line": { "type": "integer", "minimum": 1 },
44
+ "content": { "type": "string" },
45
+ "reason": { "type": "string" },
46
+ "kind": {
47
+ "type": "string",
48
+ "description": "Optional structural label from a disambiguating tool (e.g. ast-grep): member, object_key, sql_string, call, comment, unknown. Absent when the slice came from lexical search alone."
49
+ },
50
+ "hit_count": {
51
+ "type": "integer",
52
+ "minimum": 1,
53
+ "description": "Optional: how many raw search hits this slice's line range covers, when built via from_matches()."
54
+ }
55
+ }
56
+ }
57
+ },
58
+ "allowed_diagnostics": {
59
+ "type": "array",
60
+ "description": "Pre-approved read-only commands the worker may run verbatim. Empty/omitted means none.",
61
+ "items": { "type": "string" },
62
+ "default": []
63
+ },
64
+ "ceilings": {
65
+ "type": "object",
66
+ "additionalProperties": false,
67
+ "required": ["max_follow_up_reads", "max_output_tokens", "timeout_seconds"],
68
+ "properties": {
69
+ "max_follow_up_reads": { "type": "integer", "minimum": 0, "maximum": 2 },
70
+ "max_output_tokens": { "type": "integer", "minimum": 1 },
71
+ "timeout_seconds": { "type": "integer", "minimum": 1, "maximum": 600 }
72
+ }
73
+ },
74
+ "response_schema_ref": {
75
+ "type": "string",
76
+ "const": "./finding-report.schema.json"
77
+ }
78
+ }
79
+ }
@@ -1,40 +1,46 @@
1
- """Provider-neutral contracts for indexing and retrieval."""
1
+ """Provider-neutral contracts for indexing and retrieval.
2
+
3
+ Portable knowledge is repository-scoped. ``workspace_id`` remains optional compatibility
4
+ metadata only; it is never required to identify repository knowledge.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
5
9
  from dataclasses import dataclass, field
6
- from typing import Any, Mapping, Sequence
10
+ from typing import Any, Mapping
7
11
 
8
12
  JsonMap = Mapping[str, Any]
9
13
 
14
+
10
15
  @dataclass(frozen=True, slots=True)
11
16
  class Source:
12
17
  source_id: str
13
18
  source_type: str
14
- workspace_id: str
15
19
  uri: str
16
20
  repository_full_name: str | None = None
17
21
  ref: str | None = None
18
22
  commit_sha: str | None = None
23
+ workspace_id: str | None = None
19
24
  metadata: JsonMap = field(default_factory=dict)
20
25
 
26
+
21
27
  @dataclass(frozen=True, slots=True)
22
28
  class Document:
23
29
  document_id: str
24
30
  source_id: str
25
- workspace_id: str
26
31
  content_hash: str
27
32
  media_type: str
28
33
  title: str | None = None
29
34
  path: str | None = None
30
35
  text: str | None = None
36
+ workspace_id: str | None = None
31
37
  metadata: JsonMap = field(default_factory=dict)
32
38
 
39
+
33
40
  @dataclass(frozen=True, slots=True)
34
41
  class Chunk:
35
42
  chunk_id: str
36
43
  document_id: str
37
- workspace_id: str
38
44
  content: str
39
45
  ordinal: int
40
46
  content_hash: str
@@ -45,25 +51,27 @@ class Chunk:
45
51
  path: str | None = None
46
52
  symbol: str | None = None
47
53
  heading_path: tuple[str, ...] = ()
54
+ workspace_id: str | None = None
48
55
  metadata: JsonMap = field(default_factory=dict)
49
56
 
57
+
50
58
  @dataclass(frozen=True, slots=True)
51
59
  class RepositoryDescriptor:
52
60
  repository_id: str
53
61
  full_name: str
54
- workspace_id: str
55
62
  default_branch: str
56
63
  product_ids: tuple[str, ...] = ()
57
64
  indexed_refs: tuple[str, ...] = ()
58
65
  enabled_lanes: tuple[str, ...] = ("code", "docs")
59
66
  include: tuple[str, ...] = ()
60
67
  exclude: tuple[str, ...] = ()
68
+ workspace_id: str | None = None
61
69
  metadata: JsonMap = field(default_factory=dict)
62
70
 
71
+
63
72
  @dataclass(frozen=True, slots=True)
64
73
  class RetrievalQuery:
65
74
  text: str
66
- workspace_id: str
67
75
  repositories: tuple[str, ...] = ()
68
76
  product_ids: tuple[str, ...] = ()
69
77
  refs: tuple[str, ...] = ()
@@ -74,6 +82,8 @@ class RetrievalQuery:
74
82
  require_current_ref: bool = True
75
83
  include_archived: bool = False
76
84
  explain: bool = True
85
+ workspace_id: str | None = None
86
+
77
87
 
78
88
  @dataclass(frozen=True, slots=True)
79
89
  class RetrievalHit:
@@ -92,6 +102,7 @@ class RetrievalHit:
92
102
  stale: bool = False
93
103
  metadata: JsonMap = field(default_factory=dict)
94
104
 
105
+
95
106
  @dataclass(frozen=True, slots=True)
96
107
  class ContextPack:
97
108
  query_id: str
@@ -101,6 +112,7 @@ class ContextPack:
101
112
  confidence: str = "unknown"
102
113
  diagnostics: JsonMap = field(default_factory=dict)
103
114
 
115
+
104
116
  @dataclass(frozen=True, slots=True)
105
117
  class IngestReceipt:
106
118
  run_id: str
@@ -1,3 +1,3 @@
1
- from agentsam_sdk.repository.inventory import main
1
+ from agentsam_sdk.repository.intelligence.cli import main_cli
2
2
 
3
- raise SystemExit(main())
3
+ raise SystemExit(main_cli())
@@ -0,0 +1,28 @@
1
+ """Bounded recon-worker harness: task-packet construction and finding-report validation.
2
+
3
+ This module does not call any model. It is the deterministic controller half of the
4
+ recon protocol described in docs/RECON.md — building small, exact, already-scoped work
5
+ packets, and validating whatever a worker (local or hosted) sends back against hard
6
+ ceilings before it reaches a capable coding agent.
7
+ """
8
+ from .packet import (
9
+ build_task_packet,
10
+ from_matches,
11
+ from_ast_grep,
12
+ from_ripgrep,
13
+ PacketError,
14
+ TOOL_NAME_PACK,
15
+ )
16
+ from .validate import validate_report, ReportError, TOOL_NAME_VALIDATE
17
+
18
+ __all__ = [
19
+ "build_task_packet",
20
+ "from_matches",
21
+ "from_ast_grep",
22
+ "from_ripgrep",
23
+ "PacketError",
24
+ "TOOL_NAME_PACK",
25
+ "validate_report",
26
+ "ReportError",
27
+ "TOOL_NAME_VALIDATE",
28
+ ]
@@ -0,0 +1,3 @@
1
+ from .cli import main_cli
2
+
3
+ raise SystemExit(main_cli())
@@ -0,0 +1,178 @@
1
+ """CLI and ToolInput adapter for the recon bounded-worker harness.
2
+
3
+ Two read-only operations, matching the controller/validator halves of the pipeline
4
+ described in docs/RECON.md:
5
+
6
+ python -m agentsam_sdk.repository.recon pack --repo-root . \\
7
+ --question "..." --slice backend/workflows/repository/workflows.js:1-180 \\
8
+ --out packet.json
9
+
10
+ python -m agentsam_sdk.repository.recon validate --packet packet.json \\
11
+ --report report.json
12
+
13
+ Neither operation calls a model. `pack` is the deterministic controller step;
14
+ `validate` is the deterministic gate a worker's report must pass before a capable
15
+ agent ever sees it.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ from pathlib import Path
22
+
23
+ from agentsam_sdk.runtime.contract import ToolInput, ToolResult, start_timer, write_receipt
24
+
25
+ from .packet import TOOL_NAME_PACK, build_task_packet
26
+ from .validate import TOOL_NAME_VALIDATE, ReportError, validate_report
27
+
28
+
29
+ def _parse_slice_arg(raw: str) -> dict[str, str | int]:
30
+ # path[:start-end]
31
+ if ":" in raw:
32
+ path, rng = raw.rsplit(":", 1)
33
+ if "-" in rng:
34
+ start, end = rng.split("-", 1)
35
+ return {"path": path, "start_line": int(start), "end_line": int(end)}
36
+ return {"path": raw}
37
+
38
+
39
+ def run_pack(tool_input: ToolInput) -> ToolResult:
40
+ started = start_timer()
41
+ try:
42
+ tool_input.assert_read_only()
43
+ params = tool_input.params
44
+ packet = build_task_packet(
45
+ params.get("repo_root") or ".",
46
+ question=params["question"],
47
+ slices=params.get("slices") or [],
48
+ task_id=params.get("task_id"),
49
+ allowed_diagnostics=params.get("allowed_diagnostics") or (),
50
+ max_follow_up_reads=int(params.get("max_follow_up_reads", 2)),
51
+ max_output_tokens=int(params.get("max_output_tokens", 800)),
52
+ timeout_seconds=int(params.get("timeout_seconds", 90)),
53
+ )
54
+ artifacts: list[str] = []
55
+ output_dir = tool_input.output_path()
56
+ if output_dir:
57
+ output_dir.mkdir(parents=True, exist_ok=True)
58
+ out = output_dir / f"recon-packet-{packet['task_id']}.json"
59
+ out.write_text(json.dumps(packet, indent=2), encoding="utf-8")
60
+ artifacts.append(str(out))
61
+ result = ToolResult(
62
+ ok=True,
63
+ tool=TOOL_NAME_PACK,
64
+ mode=tool_input.mode or "read-only",
65
+ request_id=tool_input.request_id,
66
+ started_at=started,
67
+ finished_at=start_timer(),
68
+ summary=f"Built bounded task packet {packet['task_id']} ({len(packet['slices'])} slice(s)).",
69
+ data=packet,
70
+ artifacts=artifacts,
71
+ )
72
+ except Exception as exc: # noqa: BLE001 - normalized into ToolResult
73
+ result = ToolResult(
74
+ ok=False,
75
+ tool=TOOL_NAME_PACK,
76
+ mode=tool_input.mode or "read-only",
77
+ request_id=tool_input.request_id,
78
+ started_at=started,
79
+ finished_at=start_timer(),
80
+ summary="recon pack failed",
81
+ error=str(exc)[:500],
82
+ )
83
+ write_receipt(result, tool_input.output_path())
84
+ return result
85
+
86
+
87
+ def run_validate(tool_input: ToolInput) -> ToolResult:
88
+ started = start_timer()
89
+ try:
90
+ tool_input.assert_read_only()
91
+ params = tool_input.params
92
+ packet = params["packet"]
93
+ report = params["report"]
94
+ try:
95
+ validated = validate_report(report, packet)
96
+ ok = True
97
+ summary = f"Report for {report.get('task_id')} is valid ({report.get('status')})."
98
+ error = None
99
+ except ReportError as exc:
100
+ validated = {}
101
+ ok = False
102
+ summary = "Report rejected; do not forward to the capable agent."
103
+ error = str(exc)
104
+ result = ToolResult(
105
+ ok=ok,
106
+ tool=TOOL_NAME_VALIDATE,
107
+ mode=tool_input.mode or "read-only",
108
+ request_id=tool_input.request_id,
109
+ started_at=started,
110
+ finished_at=start_timer(),
111
+ summary=summary,
112
+ data={"validated_report": validated} if ok else {},
113
+ error=error,
114
+ )
115
+ except Exception as exc: # noqa: BLE001 - normalized into ToolResult
116
+ result = ToolResult(
117
+ ok=False,
118
+ tool=TOOL_NAME_VALIDATE,
119
+ mode=tool_input.mode or "read-only",
120
+ request_id=tool_input.request_id,
121
+ started_at=started,
122
+ finished_at=start_timer(),
123
+ summary="recon validate failed",
124
+ error=str(exc)[:500],
125
+ )
126
+ write_receipt(result, tool_input.output_path())
127
+ return result
128
+
129
+
130
+ def main_cli(argv: list[str] | None = None) -> int:
131
+ parser = argparse.ArgumentParser(
132
+ prog="python -m agentsam_sdk.repository.recon",
133
+ description="Build bounded recon task packets and validate worker findings.",
134
+ )
135
+ sub = parser.add_subparsers(dest="command", required=True)
136
+
137
+ pack_p = sub.add_parser("pack", help="Build a bounded ReconTaskPacket.")
138
+ pack_p.add_argument("--repo-root", default=".")
139
+ pack_p.add_argument("--question", required=True)
140
+ pack_p.add_argument(
141
+ "--slice", dest="slices", action="append", default=[],
142
+ help="path[:start-end], repeatable, up to 5.",
143
+ )
144
+ pack_p.add_argument("--task-id")
145
+ pack_p.add_argument("--out")
146
+
147
+ validate_p = sub.add_parser("validate", help="Validate a ReconFindingReport against its packet.")
148
+ validate_p.add_argument("--packet", required=True, help="Path to a packet JSON file.")
149
+ validate_p.add_argument("--report", required=True, help="Path to a report JSON file.")
150
+
151
+ args = parser.parse_args(argv)
152
+
153
+ if args.command == "pack":
154
+ packet = build_task_packet(
155
+ args.repo_root,
156
+ question=args.question,
157
+ slices=[_parse_slice_arg(s) for s in args.slices],
158
+ task_id=args.task_id,
159
+ )
160
+ rendered = json.dumps(packet, indent=2)
161
+ if args.out:
162
+ Path(args.out).expanduser().write_text(rendered, encoding="utf-8")
163
+ else:
164
+ print(rendered)
165
+ return 0
166
+
167
+ if args.command == "validate":
168
+ packet = json.loads(Path(args.packet).expanduser().read_text(encoding="utf-8"))
169
+ report = json.loads(Path(args.report).expanduser().read_text(encoding="utf-8"))
170
+ try:
171
+ validate_report(report, packet)
172
+ except ReportError as exc:
173
+ print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
174
+ return 1
175
+ print(json.dumps({"ok": True}, indent=2))
176
+ return 0
177
+
178
+ return 2