workflow-toolkit 1.0.1
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.
- package/.agents/skills/ponytail/SKILL.md +111 -0
- package/.agents/skills/ponytail-audit/SKILL.md +37 -0
- package/.agents/skills/ponytail-debt/SKILL.md +41 -0
- package/.agents/skills/ponytail-gain/SKILL.md +47 -0
- package/.agents/skills/ponytail-help/SKILL.md +70 -0
- package/.agents/skills/ponytail-review/SKILL.md +52 -0
- package/.agents/skills/prompt-review/SKILL.md +60 -0
- package/.agents/skills/wtk/SKILL.md +64 -0
- package/.agents/skills/wtk/references/artifacts.md +82 -0
- package/.agents/skills/wtk/references/evidence.md +68 -0
- package/.agents/skills/wtk/references/git.md +75 -0
- package/.agents/skills/wtk/references/test-contract.md +76 -0
- package/.agents/skills/wtk/references/validation.md +127 -0
- package/.agents/skills/wtk-config/SKILL.md +102 -0
- package/.agents/skills/wtk-config/assets/agents/claude/deep-reviewer.md +40 -0
- package/.agents/skills/wtk-config/assets/agents/claude/designer.md +57 -0
- package/.agents/skills/wtk-config/assets/agents/claude/explorer.md +41 -0
- package/.agents/skills/wtk-config/assets/agents/claude/implementer.md +57 -0
- package/.agents/skills/wtk-config/assets/agents/claude/planner.md +68 -0
- package/.agents/skills/wtk-config/assets/agents/claude/verifier.md +80 -0
- package/.agents/skills/wtk-config/assets/agents/codex/deep-reviewer.toml +41 -0
- package/.agents/skills/wtk-config/assets/agents/codex/designer.toml +55 -0
- package/.agents/skills/wtk-config/assets/agents/codex/explorer.toml +35 -0
- package/.agents/skills/wtk-config/assets/agents/codex/implementer.toml +52 -0
- package/.agents/skills/wtk-config/assets/agents/codex/planner.toml +66 -0
- package/.agents/skills/wtk-config/assets/agents/codex/verifier.toml +78 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/deep-reviewer.md +38 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/designer.md +55 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/explorer.md +39 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/implementer.md +55 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/planner.md +66 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/verifier.md +79 -0
- package/.agents/skills/wtk-config/scripts/ad-index.py +123 -0
- package/.agents/skills/wtk-config/scripts/repository_intelligence.py +671 -0
- package/.agents/skills/wtk-config/scripts/workflow_config.py +972 -0
- package/.agents/skills/wtk-deep-review/SKILL.md +164 -0
- package/.agents/skills/wtk-deep-review/assets/PROMPT.md +56 -0
- package/.agents/skills/wtk-deep-review/assets/REVIEW_UI.html +1340 -0
- package/.agents/skills/wtk-deep-review/assets/findings.schema.json +113 -0
- package/.agents/skills/wtk-deep-review/references/context-pack.md +88 -0
- package/.agents/skills/wtk-deep-review/references/orchestration.md +164 -0
- package/.agents/skills/wtk-deep-review/references/output-contracts.md +149 -0
- package/.agents/skills/wtk-deep-review/references/publish-github.md +83 -0
- package/.agents/skills/wtk-deep-review/references/state-and-learnings.md +64 -0
- package/.agents/skills/wtk-deep-review/references/subagent-runtimes.md +33 -0
- package/.agents/skills/wtk-deep-review/references/taxonomy.md +70 -0
- package/.agents/skills/wtk-deep-review/scripts/_common.py +371 -0
- package/.agents/skills/wtk-deep-review/scripts/build_jobs.py +561 -0
- package/.agents/skills/wtk-deep-review/scripts/build_knowledge.py +349 -0
- package/.agents/skills/wtk-deep-review/scripts/build_manifest.py +479 -0
- package/.agents/skills/wtk-deep-review/scripts/graft_context.py +160 -0
- package/.agents/skills/wtk-deep-review/scripts/graphify_context.py +75 -0
- package/.agents/skills/wtk-deep-review/scripts/merge_findings.py +313 -0
- package/.agents/skills/wtk-deep-review/scripts/render_html.py +240 -0
- package/.agents/skills/wtk-deep-review/scripts/render_review.py +282 -0
- package/.agents/skills/wtk-deep-review/scripts/run_jobs.py +436 -0
- package/.agents/skills/wtk-deep-review/scripts/token_metrics.py +369 -0
- package/.agents/skills/wtk-discover/SKILL.md +270 -0
- package/.agents/skills/wtk-discover/references/document-format.md +149 -0
- package/.agents/skills/wtk-implement/SKILL.md +95 -0
- package/.agents/skills/wtk-implement/references/checklist-format.md +116 -0
- package/.agents/skills/wtk-implement/references/screens.md +129 -0
- package/.agents/skills/wtk-implement/references/test-policy.md +152 -0
- package/.agents/skills/wtk-implement/references/verify.md +297 -0
- package/.agents/skills/wtk-knowledge-check/SKILL.md +16 -0
- package/.agents/skills/wtk-knowledge-check/scripts/check.ts +502 -0
- package/.agents/skills/wtk-knowledge-check/scripts/cli.ts +32 -0
- package/.agents/skills/wtk-knowledge-check/scripts/frontmatter.ts +44 -0
- package/.agents/skills/wtk-lean/.skill-meta.json +6 -0
- package/.agents/skills/wtk-lean/NOTICE.md +18 -0
- package/.agents/skills/wtk-lean/SKILL.md +137 -0
- package/.agents/skills/wtk-lean/references/build.md +168 -0
- package/.agents/skills/wtk-lean/references/checks.md +253 -0
- package/.agents/skills/wtk-lean/references/memory.md +156 -0
- package/.agents/skills/wtk-lean/references/plan.md +514 -0
- package/.agents/skills/wtk-lean/references/verify.md +336 -0
- package/.agents/skills/wtk-lean/scripts/check_commit.py +121 -0
- package/.agents/skills/wtk-lean/scripts/fixtures/checks.md +98 -0
- package/.agents/skills/wtk-lean/scripts/fixtures/plan.md +119 -0
- package/.agents/skills/wtk-lean/scripts/fixtures/verification.md +41 -0
- package/.agents/skills/wtk-lean/scripts/lessons.py +412 -0
- package/.agents/skills/wtk-lean/scripts/selftest.py +401 -0
- package/.agents/skills/wtk-lean/scripts/validate_checks.py +417 -0
- package/.agents/skills/wtk-lean/scripts/validate_plan.py +580 -0
- package/.agents/skills/wtk-lean/scripts/validate_verification.py +357 -0
- package/.agents/skills/wtk-plan/SKILL.md +102 -0
- package/.agents/skills/wtk-plan/references/document-format.md +222 -0
- package/.agents/skills/wtk-qa/SKILL.md +14 -0
- package/.agents/skills/wtk-qa-execute/SKILL.md +115 -0
- package/.agents/skills/wtk-qa-execute/references/fix-loop.md +23 -0
- package/.agents/skills/wtk-qa-execute/references/session-protocol.md +25 -0
- package/.agents/skills/wtk-qa-plan/SKILL.md +111 -0
- package/.agents/skills/wtk-qa-plan/references/profile.md +35 -0
- package/.agents/skills/wtk-ship/SKILL.md +49 -0
- package/.agents/skills/wtk-ship/remediation.py +170 -0
- package/.agents/skills/wtk-ship/scripts/close_feature.py +83 -0
- package/.agents/skills/wtk-ship/scripts/review_convergence.py +373 -0
- package/.wtk.toml.example +93 -0
- package/AGENTS.md +129 -0
- package/NOTICE.md +12 -0
- package/README.md +440 -0
- package/bin/wtk.js +25 -0
- package/docs/toolkit/README.md +56 -0
- package/docs/toolkit/decisions.md +61 -0
- package/docs/toolkit/guidelines/CONTEXT-BUDGET.md +64 -0
- package/docs/toolkit/guidelines/DX.md +67 -0
- package/docs/toolkit/guidelines/FRONTEND.md +73 -0
- package/docs/toolkit/guidelines/KNOWLEDGE-WIKI.md +128 -0
- package/docs/toolkit/guidelines/MODELING.md +134 -0
- package/docs/toolkit/guidelines/QA-EXECUTION.md +46 -0
- package/docs/toolkit/guidelines/QA-SCENARIOS.md +165 -0
- package/docs/toolkit/guidelines/REVIEW-ROUNDS.md +160 -0
- package/docs/toolkit/guidelines/SECURITY.md +160 -0
- package/docs/toolkit/guidelines/UI-UX.md +117 -0
- package/docs/toolkit/guidelines/WORKFLOW-MEMORY.md +73 -0
- package/docs/toolkit/guidelines.md +50 -0
- package/docs/toolkit/loop.md +77 -0
- package/docs/toolkit/purpose.md +51 -0
- package/docs/toolkit/repository-intelligence.md +53 -0
- package/docs/toolkit/reviews.md +77 -0
- package/knowledge/AGENTS.md +316 -0
- package/knowledge/raw/README.md +17 -0
- package/package.json +74 -0
- package/scripts/install_security_skills.py +816 -0
- package/scripts/installer/engine.js +254 -0
- package/scripts/installer/knowledge.js +28 -0
- package/scripts/installer/packets.js +77 -0
- package/scripts/installer/terminal.js +98 -0
- package/scripts/installer/transaction.js +75 -0
- package/skills-lock.json +92 -0
- package/templates/adoption/agents/core.md +23 -0
- package/templates/adoption/agents/quality.md +3 -0
- package/templates/adoption/knowledge/wiki/architecture/index.md +3 -0
- package/templates/adoption/knowledge/wiki/decisions/index.md +3 -0
- package/templates/adoption/knowledge/wiki/design/index.md +3 -0
- package/templates/adoption/knowledge/wiki/domain/index.md +3 -0
- package/templates/adoption/knowledge/wiki/index.md +3 -0
- package/templates/adoption/knowledge/wiki/log.md +3 -0
- package/templates/adoption/knowledge/wiki/open-questions/index.md +3 -0
- package/templates/adoption/knowledge/wiki/product/index.md +3 -0
- package/templates/adoption/knowledge/wiki/research/index.md +3 -0
- package/templates/adoption/product/AGENT-CONTEXT.md +25 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Safe, checkout-local routing adapter for Graft and Graphify."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import contextlib
|
|
8
|
+
import fcntl
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
import stat
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
import time
|
|
20
|
+
from typing import Any, Iterator, Sequence
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
GRAFT_VERSION = "0.10.1"
|
|
24
|
+
GRAPHIFY_VERSION = "0.9.14"
|
|
25
|
+
DEGRADED_EXIT = 3
|
|
26
|
+
SCHEMA = 1
|
|
27
|
+
STATE_DIR = ".repository-intelligence"
|
|
28
|
+
MAX_CONTEXT_CHARS = 12_000
|
|
29
|
+
MAX_DURATION_SECONDS = 120
|
|
30
|
+
GRAFT_OPERATIONS = {"ask", "skeleton", "callers", "grep", "map"}
|
|
31
|
+
GRAPHIFY_OPERATIONS = {"query", "path", "explain", "affected"}
|
|
32
|
+
GRAPHIFY_BACKENDS = {
|
|
33
|
+
"code-only", "gemini", "kimi", "claude", "openai", "deepseek", "ollama",
|
|
34
|
+
"bedrock", "claude-cli", "azure",
|
|
35
|
+
}
|
|
36
|
+
REMOTE_BACKENDS = {"gemini", "kimi", "openai", "deepseek", "bedrock", "azure"}
|
|
37
|
+
ARCHITECTURAL_TRIGGERS = {
|
|
38
|
+
"boundary", "boundary_change", "domain", "domain_boundary", "shared_abstraction",
|
|
39
|
+
"responsibility_transfer", "central_flow", "architectural_uncertainty",
|
|
40
|
+
"residual_architectural_uncertainty", "architecture_risk", "module_boundary",
|
|
41
|
+
}
|
|
42
|
+
CONTROL_FIELDS = ("tree", "prompt_hash", "provider", "model", "effort", "acceptance_contract_hash")
|
|
43
|
+
METRIC_FIELDS = (
|
|
44
|
+
"input_tokens", "output_tokens", "total_tokens", "repository_intelligence_calls",
|
|
45
|
+
"native_search_calls", "direct_files_read", "wall_clock_ms", "rework_count",
|
|
46
|
+
"review_findings",
|
|
47
|
+
)
|
|
48
|
+
BENCHMARK_CONFIGURATIONS = {"baseline", "graft", "routed"}
|
|
49
|
+
REMOVAL_SURFACES = {"routing", "provisioning", "configuration", "generated_state", "qa_promises"}
|
|
50
|
+
DEGRADED_FALLBACK = "targeted-native-inspection"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class IntelligenceError(Exception):
|
|
54
|
+
"""An expected degraded result, safe to show to an operator."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, reason: str, *, actual: str | None = None, expected: str | None = None):
|
|
57
|
+
super().__init__(reason)
|
|
58
|
+
self.reason = reason
|
|
59
|
+
self.actual = actual
|
|
60
|
+
self.expected = expected
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _run(command: Sequence[str], root: Path, *, timeout: float = MAX_DURATION_SECONDS,
|
|
64
|
+
env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
|
|
65
|
+
"""Run an argument vector. Shell evaluation is deliberately unavailable."""
|
|
66
|
+
try:
|
|
67
|
+
return subprocess.run(
|
|
68
|
+
list(command), cwd=root, env=env, text=True, capture_output=True,
|
|
69
|
+
check=False, timeout=timeout,
|
|
70
|
+
)
|
|
71
|
+
except subprocess.TimeoutExpired as exc:
|
|
72
|
+
raise IntelligenceError(f"{Path(command[0]).name} timed out") from exc
|
|
73
|
+
except (OSError, ValueError) as exc:
|
|
74
|
+
raise IntelligenceError(f"{Path(command[0]).name} execution failed") from exc
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _redact(value: str) -> str:
|
|
78
|
+
"""Keep tool diagnostics content-safe, including credential-like environment values."""
|
|
79
|
+
result = value
|
|
80
|
+
for key, secret in os.environ.items():
|
|
81
|
+
if secret and re.search(r"(?:TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL)", key, re.I):
|
|
82
|
+
result = result.replace(secret, "[REDACTED]")
|
|
83
|
+
result = re.sub(rf"\b{re.escape(key)}\b", "[REDACTED]", result, flags=re.I)
|
|
84
|
+
return re.sub(r"\b[A-Z][A-Z0-9_]*(?:TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL)[A-Z0-9_]*\b", "[REDACTED]", result)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def checkout_root(value: str | Path) -> Path:
|
|
88
|
+
root = Path(value).expanduser().resolve()
|
|
89
|
+
if not root.is_dir():
|
|
90
|
+
raise ValueError("checkout root is not a directory")
|
|
91
|
+
result = subprocess.run(
|
|
92
|
+
["git", "rev-parse", "--show-toplevel"], cwd=root, text=True,
|
|
93
|
+
capture_output=True, check=False,
|
|
94
|
+
)
|
|
95
|
+
if result.returncode != 0:
|
|
96
|
+
raise ValueError("checkout root is not a Git repository")
|
|
97
|
+
git_root = Path(result.stdout.strip()).resolve()
|
|
98
|
+
if git_root != root:
|
|
99
|
+
raise ValueError("checkout root must be the Git repository root")
|
|
100
|
+
return root
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def tree_fingerprint(root: Path) -> str:
|
|
104
|
+
"""Return an AD-018-style tree object without modifying the checkout index."""
|
|
105
|
+
result = subprocess.run(
|
|
106
|
+
["git", "rev-parse", "--git-path", "index"], cwd=root, text=True,
|
|
107
|
+
capture_output=True, check=True,
|
|
108
|
+
)
|
|
109
|
+
real = Path(result.stdout.strip())
|
|
110
|
+
if not real.is_absolute():
|
|
111
|
+
real = root / real
|
|
112
|
+
with tempfile.TemporaryDirectory(prefix="repository-intelligence-") as raw:
|
|
113
|
+
temporary = Path(raw) / "index"
|
|
114
|
+
if real.is_file():
|
|
115
|
+
shutil.copy2(real, temporary)
|
|
116
|
+
env = {**os.environ, "GIT_INDEX_FILE": str(temporary)}
|
|
117
|
+
subprocess.run(["git", "add", "-A"], cwd=root, env=env, check=True, capture_output=True)
|
|
118
|
+
subprocess.run(
|
|
119
|
+
["git", "rm", "-rf", "--cached", "--ignore-unmatch", "-q", "--", STATE_DIR],
|
|
120
|
+
cwd=root, env=env, check=False, capture_output=True,
|
|
121
|
+
)
|
|
122
|
+
return subprocess.run(
|
|
123
|
+
["git", "write-tree"], cwd=root, env=env, text=True,
|
|
124
|
+
capture_output=True, check=True,
|
|
125
|
+
).stdout.strip()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _state_dir(root: Path) -> Path:
|
|
129
|
+
path = root / STATE_DIR
|
|
130
|
+
path.mkdir(mode=0o700, exist_ok=True)
|
|
131
|
+
path.chmod(stat.S_IRWXU)
|
|
132
|
+
return path
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _state_path(root: Path, tool: str) -> Path:
|
|
136
|
+
return _state_dir(root) / f"{tool}.json"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def read_state(root: Path, tool: str) -> dict[str, Any] | None:
|
|
140
|
+
try:
|
|
141
|
+
value = json.loads(_state_path(root, tool).read_text(encoding="utf-8"))
|
|
142
|
+
except (OSError, ValueError):
|
|
143
|
+
return None
|
|
144
|
+
return value if isinstance(value, dict) else None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _write_json(path: Path, value: dict[str, Any]) -> None:
|
|
148
|
+
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
|
149
|
+
try:
|
|
150
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
151
|
+
json.dump(value, handle, sort_keys=True, separators=(",", ":"))
|
|
152
|
+
handle.write("\n")
|
|
153
|
+
handle.flush()
|
|
154
|
+
os.fsync(handle.fileno())
|
|
155
|
+
os.replace(name, path)
|
|
156
|
+
finally:
|
|
157
|
+
Path(name).unlink(missing_ok=True)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@contextlib.contextmanager
|
|
161
|
+
def mutation_lock(root: Path) -> Iterator[None]:
|
|
162
|
+
path = _state_dir(root) / "mutation.lock"
|
|
163
|
+
fd = os.open(path, os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0), 0o600)
|
|
164
|
+
try:
|
|
165
|
+
fcntl.flock(fd, fcntl.LOCK_EX)
|
|
166
|
+
yield
|
|
167
|
+
finally:
|
|
168
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
169
|
+
os.close(fd)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@contextlib.contextmanager
|
|
173
|
+
def read_lock(root: Path) -> Iterator[None]:
|
|
174
|
+
"""Allow concurrent reads of a completed representation, excluding mutation."""
|
|
175
|
+
path = _state_dir(root) / "mutation.lock"
|
|
176
|
+
fd = os.open(path, os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0), 0o600)
|
|
177
|
+
try:
|
|
178
|
+
fcntl.flock(fd, fcntl.LOCK_SH)
|
|
179
|
+
yield
|
|
180
|
+
finally:
|
|
181
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
182
|
+
os.close(fd)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _tool_path(root: Path, tool: str) -> str | None:
|
|
186
|
+
local = root / "node_modules" / ".bin" / tool
|
|
187
|
+
if local.is_file() and os.access(local, os.X_OK) and not _foreign_node_modules_binary(root, local, tool):
|
|
188
|
+
return str(local)
|
|
189
|
+
candidates = (tool, "graphifyy") if tool == "graphify" else (tool,)
|
|
190
|
+
for candidate in candidates:
|
|
191
|
+
found = shutil.which(candidate)
|
|
192
|
+
if found and not _foreign_node_modules_binary(root, found, tool):
|
|
193
|
+
return found
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _foreign_node_modules_binary(root: Path, value: str | Path, tool: str) -> bool:
|
|
198
|
+
"""Reject Graft binaries surfaced from another checkout's package bin directory."""
|
|
199
|
+
if tool != "graft":
|
|
200
|
+
return False
|
|
201
|
+
lexical = Path(value).absolute()
|
|
202
|
+
local_modules = (root / "node_modules").resolve()
|
|
203
|
+
if lexical.parent.name == ".bin" and lexical.parent.parent.name == "node_modules":
|
|
204
|
+
try:
|
|
205
|
+
lexical.relative_to(local_modules)
|
|
206
|
+
except ValueError:
|
|
207
|
+
return True
|
|
208
|
+
try:
|
|
209
|
+
resolved = Path(value).resolve(strict=True)
|
|
210
|
+
except OSError:
|
|
211
|
+
return True
|
|
212
|
+
try:
|
|
213
|
+
resolved.relative_to(local_modules)
|
|
214
|
+
except ValueError:
|
|
215
|
+
if "node_modules" in resolved.parts:
|
|
216
|
+
return True
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def tool_version(root: Path, tool: str) -> tuple[str | None, str | None]:
|
|
221
|
+
binary = _tool_path(root, tool)
|
|
222
|
+
if not binary:
|
|
223
|
+
return None, None
|
|
224
|
+
try:
|
|
225
|
+
result = _run([binary, "--version"], root)
|
|
226
|
+
except IntelligenceError:
|
|
227
|
+
return None, binary
|
|
228
|
+
match = re.search(r"\b(\d+\.\d+\.\d+)\b", result.stdout + "\n" + result.stderr)
|
|
229
|
+
return (match.group(1) if match else None), binary
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _require_tool(root: Path, tool: str, expected: str) -> str:
|
|
233
|
+
actual, binary = tool_version(root, tool)
|
|
234
|
+
if binary is None:
|
|
235
|
+
command = "npm install --save-dev --save-exact @nanonets/graft@0.10.1" if tool == "graft" else "uv tool install graphifyy==0.9.14"
|
|
236
|
+
raise IntelligenceError(f"{tool} unavailable; install with: {command}", expected=expected)
|
|
237
|
+
if actual != expected:
|
|
238
|
+
raise IntelligenceError(f"{tool} version mismatch: expected {expected}, got {actual or 'unknown'}", actual=actual, expected=expected)
|
|
239
|
+
return binary
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _base_state(root: Path, tool: str, version: str, *, backend: str = "not-applicable",
|
|
243
|
+
status: str = "ready") -> dict[str, Any]:
|
|
244
|
+
manifest = _source_manifest(root)
|
|
245
|
+
return {
|
|
246
|
+
"schema": SCHEMA, "tool": tool, "tool_version": version,
|
|
247
|
+
"checkout": str(root), "tree": tree_fingerprint(root), "backend": backend,
|
|
248
|
+
"source_scope": ["."], "indexed_source_manifest": manifest,
|
|
249
|
+
"source_fingerprint": _source_fingerprint(root), "status": status,
|
|
250
|
+
"built_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _source_manifest(root: Path) -> list[str]:
|
|
255
|
+
listed = subprocess.run(
|
|
256
|
+
["git", "ls-files", "-co", "--exclude-standard"], cwd=root, text=True,
|
|
257
|
+
capture_output=True, check=True,
|
|
258
|
+
).stdout.splitlines()
|
|
259
|
+
return [
|
|
260
|
+
path for path in listed
|
|
261
|
+
if (root / path).exists() and path not in {STATE_DIR, f"{STATE_DIR}/"} and not path.startswith(f"{STATE_DIR}/")
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _source_fingerprint(root: Path) -> str:
|
|
266
|
+
digest = hashlib.sha256()
|
|
267
|
+
for path in _source_manifest(root):
|
|
268
|
+
digest.update(path.encode("utf-8"))
|
|
269
|
+
digest.update(b"\0")
|
|
270
|
+
source = root / path
|
|
271
|
+
if stat.S_ISLNK(os.lstat(source).st_mode):
|
|
272
|
+
content = os.readlink(source).encode("utf-8", "surrogateescape")
|
|
273
|
+
else:
|
|
274
|
+
content = source.read_bytes()
|
|
275
|
+
digest.update(content)
|
|
276
|
+
digest.update(b"\0")
|
|
277
|
+
return digest.hexdigest()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _foreign_state(root: Path, state: dict[str, Any] | None) -> bool:
|
|
281
|
+
if not state or state.get("checkout") != str(root):
|
|
282
|
+
return bool(state)
|
|
283
|
+
tree = state.get("tree")
|
|
284
|
+
current = tree_fingerprint(root)
|
|
285
|
+
if tree == current:
|
|
286
|
+
return False
|
|
287
|
+
# A source edit makes the representation stale and refreshable; an unchanged source
|
|
288
|
+
# paired with another tree fingerprint is foreign state and must be rejected.
|
|
289
|
+
if state.get("source_fingerprint") and state["source_fingerprint"] != _source_fingerprint(root):
|
|
290
|
+
return False
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _state_checkout_is_foreign(root: Path, state: dict[str, Any] | None) -> bool:
|
|
295
|
+
return bool(state and state.get("checkout") != str(root))
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _unavailable_state(root: Path, tool: str, reason: str, *, backend: str = "not-applicable") -> dict[str, Any]:
|
|
299
|
+
return {
|
|
300
|
+
"schema": SCHEMA,
|
|
301
|
+
"tool": tool,
|
|
302
|
+
"tool_version": GRAFT_VERSION if tool == "graft" else GRAPHIFY_VERSION,
|
|
303
|
+
"checkout": str(root),
|
|
304
|
+
"tree": tree_fingerprint(root),
|
|
305
|
+
"backend": backend,
|
|
306
|
+
"source_scope": ["."],
|
|
307
|
+
"indexed_source_manifest": [],
|
|
308
|
+
"source_fingerprint": _source_fingerprint(root),
|
|
309
|
+
"status": "unavailable",
|
|
310
|
+
"reason": _redact(reason),
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _invalidate_state(root: Path, tool: str, reason: str, *, backend: str = "not-applicable") -> None:
|
|
315
|
+
path = _state_path(root, tool)
|
|
316
|
+
try:
|
|
317
|
+
_write_json(path, _unavailable_state(root, tool, reason, backend=backend))
|
|
318
|
+
except OSError:
|
|
319
|
+
# Do not leave a previous valid fingerprint beside partially written tool output.
|
|
320
|
+
path.unlink(missing_ok=True)
|
|
321
|
+
raise
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _validate_scope(root: Path, backend: str, source_root: str | None) -> list[str]:
|
|
325
|
+
selected = Path(source_root).expanduser().resolve() if source_root else root
|
|
326
|
+
try:
|
|
327
|
+
selected.relative_to(root)
|
|
328
|
+
except ValueError as exc:
|
|
329
|
+
raise IntelligenceError("Graphify source scope is outside checkout; extraction refused") from exc
|
|
330
|
+
if backend in REMOTE_BACKENDS and source_root and selected != root:
|
|
331
|
+
raise IntelligenceError("remote Graphify source scope must be the disclosed checkout root")
|
|
332
|
+
return [str(selected.relative_to(root) or ".")]
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _bounded_output(output: str, *, limit: int = MAX_CONTEXT_CHARS) -> tuple[str, str]:
|
|
336
|
+
output = _redact(output.strip())
|
|
337
|
+
if len(output) <= limit:
|
|
338
|
+
return output, "ready"
|
|
339
|
+
lines = output.splitlines()
|
|
340
|
+
pointers = [line for line in lines if re.search(r"(?:^|[ ./])[^ ]+\.(?:py|js|ts|tsx|go|rs|java|md)(?::\d+)?\b|-->|\b(?:caller|callee|symbol|path|node|edge)\b", line, re.I)]
|
|
341
|
+
kept: list[str] = []
|
|
342
|
+
used = 0
|
|
343
|
+
for line in pointers + lines:
|
|
344
|
+
if line in kept:
|
|
345
|
+
continue
|
|
346
|
+
extra = len(line) + (1 if kept else 0)
|
|
347
|
+
if used + extra > limit:
|
|
348
|
+
continue
|
|
349
|
+
kept.append(line)
|
|
350
|
+
used += extra
|
|
351
|
+
return "\n".join(kept), "partial"
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _result(tool: str, status: str, *, context: str = "", reason: str | None = None,
|
|
355
|
+
**extra: Any) -> dict[str, Any]:
|
|
356
|
+
value = {"schema": SCHEMA, "tool": tool, "status": status, "context": context}
|
|
357
|
+
if reason:
|
|
358
|
+
value["reason"] = _redact(reason)
|
|
359
|
+
if status == "degraded":
|
|
360
|
+
value.setdefault("fallback", DEGRADED_FALLBACK)
|
|
361
|
+
value.update(extra)
|
|
362
|
+
return value
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _run_context(root: Path, tool: str, operation: str, arguments: list[str]) -> dict[str, Any]:
|
|
366
|
+
expected = GRAFT_VERSION if tool == "graft" else GRAPHIFY_VERSION
|
|
367
|
+
binary = _require_tool(root, tool, expected)
|
|
368
|
+
state = read_state(root, tool)
|
|
369
|
+
if _foreign_state(root, state):
|
|
370
|
+
return _result(tool, "degraded", reason="checkout/fingerprint mismatch; state rejected", rejected=True)
|
|
371
|
+
if tool == "graphify" and (not state or state.get("backend") in (None, "not-applicable") or state.get("status") == "unavailable"):
|
|
372
|
+
raise IntelligenceError("Graphify setup required; run graphify-setup with an explicit backend")
|
|
373
|
+
command = [binary, operation, *arguments]
|
|
374
|
+
with mutation_lock(root):
|
|
375
|
+
backend = (state or {}).get("backend", "not-applicable")
|
|
376
|
+
_invalidate_state(root, tool, "refresh in progress", backend=backend)
|
|
377
|
+
try:
|
|
378
|
+
source_root = (root / ((state or {}).get("source_scope") or ["."])[0]).resolve()
|
|
379
|
+
refresh = [binary, "build"] if tool == "graft" else [binary, "update", str(source_root)]
|
|
380
|
+
refreshed = _run(refresh, root, env={**os.environ, **({"GRAFT_REFRESH": "hash"} if tool == "graft" else {})})
|
|
381
|
+
if refreshed.returncode != 0:
|
|
382
|
+
if tool != "graphify":
|
|
383
|
+
raise IntelligenceError(f"{tool} refresh failed")
|
|
384
|
+
rebuilt = _run([binary, "update", str(source_root), "--force"], root)
|
|
385
|
+
if rebuilt.returncode != 0:
|
|
386
|
+
raise IntelligenceError("graphify refresh failed")
|
|
387
|
+
if tool == "graft":
|
|
388
|
+
checked = _run([binary, "check"], root)
|
|
389
|
+
if checked.returncode != 0:
|
|
390
|
+
raise IntelligenceError("graft stale after refresh")
|
|
391
|
+
except IntelligenceError:
|
|
392
|
+
_invalidate_state(root, tool, "repository-intelligence refresh failed", backend=backend)
|
|
393
|
+
raise
|
|
394
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
395
|
+
_invalidate_state(root, tool, "repository-intelligence refresh failed", backend=backend)
|
|
396
|
+
raise IntelligenceError(f"{tool} refresh failed") from exc
|
|
397
|
+
with read_lock(root):
|
|
398
|
+
before_query = tree_fingerprint(root)
|
|
399
|
+
output = _run(command, root)
|
|
400
|
+
after_query = tree_fingerprint(root)
|
|
401
|
+
if before_query != after_query:
|
|
402
|
+
with mutation_lock(root):
|
|
403
|
+
_invalidate_state(root, tool, "working tree changed during query", backend=backend)
|
|
404
|
+
raise IntelligenceError(f"{tool} result became stale during query")
|
|
405
|
+
if output.returncode != 0:
|
|
406
|
+
with mutation_lock(root):
|
|
407
|
+
_invalidate_state(root, tool, f"{tool} query failed", backend=backend)
|
|
408
|
+
raise IntelligenceError(f"{tool} query failed")
|
|
409
|
+
context, status = _bounded_output(output.stdout)
|
|
410
|
+
if tool == "graphify" and (state or {}).get("status") == "partial":
|
|
411
|
+
status = "partial"
|
|
412
|
+
dot_paths = [argument for argument in arguments if argument.startswith(".")]
|
|
413
|
+
if dot_paths:
|
|
414
|
+
status = "partial"
|
|
415
|
+
if not context:
|
|
416
|
+
with mutation_lock(root):
|
|
417
|
+
_invalidate_state(root, tool, f"{tool} returned insufficient context", backend=backend)
|
|
418
|
+
raise IntelligenceError(f"{tool} returned insufficient context")
|
|
419
|
+
with mutation_lock(root):
|
|
420
|
+
if tree_fingerprint(root) != after_query:
|
|
421
|
+
_invalidate_state(root, tool, "working tree changed before publication", backend=backend)
|
|
422
|
+
raise IntelligenceError(f"{tool} result became stale before publication")
|
|
423
|
+
state = _base_state(root, tool, expected, backend=backend, status=status)
|
|
424
|
+
_write_json(_state_path(root, tool), state)
|
|
425
|
+
extra: dict[str, Any] = {"tree": state["tree"], "command": command}
|
|
426
|
+
if dot_paths:
|
|
427
|
+
extra.update({"fallback": "targeted-native-inspection", "dot_paths": dot_paths})
|
|
428
|
+
return _result(tool, status, context=context, **extra)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def graphify_setup(root: Path, backend: str, mode: str, source_root: str | None = None, *, announce: bool = True) -> dict[str, Any]:
|
|
432
|
+
if backend not in GRAPHIFY_BACKENDS:
|
|
433
|
+
raise IntelligenceError(f"unsupported Graphify backend: {backend}")
|
|
434
|
+
scope = _validate_scope(root, backend, source_root)
|
|
435
|
+
binary = _require_tool(root, "graphify", GRAPHIFY_VERSION)
|
|
436
|
+
state = read_state(root, "graphify")
|
|
437
|
+
if _foreign_state(root, state):
|
|
438
|
+
return _result("graphify", "degraded", reason="checkout/fingerprint mismatch; state rejected", rejected=True)
|
|
439
|
+
if state and state.get("tree") == tree_fingerprint(root) and state.get("backend") == backend and state.get("status") != "unavailable":
|
|
440
|
+
return _result("graphify", "ready", reused=True, tree=state["tree"], backend=backend, source_scope=scope)
|
|
441
|
+
files = _source_manifest(root)
|
|
442
|
+
ignored = ["graft/", "graphify-out/", f"{STATE_DIR}/"]
|
|
443
|
+
disclosed_root = (root / scope[0]).resolve()
|
|
444
|
+
preflight = {"tool_version": GRAPHIFY_VERSION, "backend": backend, "source_root": str(disclosed_root), "indexed_file_count": len(files), "ignored_roots": ignored}
|
|
445
|
+
if announce:
|
|
446
|
+
print(json.dumps({"preflight": preflight}, sort_keys=True), flush=True)
|
|
447
|
+
with mutation_lock(root):
|
|
448
|
+
_invalidate_state(root, "graphify", "extraction in progress", backend=backend)
|
|
449
|
+
command = [binary, "extract", str(disclosed_root)]
|
|
450
|
+
if mode == "code-only":
|
|
451
|
+
command.append("--code-only")
|
|
452
|
+
else:
|
|
453
|
+
command.extend(["--mode", mode, "--backend", backend])
|
|
454
|
+
command.extend(["--out", str(root)])
|
|
455
|
+
result = _run(command, root)
|
|
456
|
+
if result.returncode != 0:
|
|
457
|
+
_invalidate_state(root, "graphify", "Graphify extraction failed", backend=backend)
|
|
458
|
+
raise IntelligenceError("Graphify extraction failed")
|
|
459
|
+
status = "partial" if mode == "code-only" else "ready"
|
|
460
|
+
new_state = _base_state(root, "graphify", GRAPHIFY_VERSION, backend=backend, status=status)
|
|
461
|
+
new_state["source_scope"] = scope
|
|
462
|
+
_write_json(_state_path(root, "graphify"), new_state)
|
|
463
|
+
return _result("graphify", status, **preflight, tree=new_state["tree"])
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def route(request: dict[str, Any]) -> dict[str, Any]:
|
|
467
|
+
"""Choose one first repository-intelligence route from a phase request."""
|
|
468
|
+
pointers = request.get("pointers") or request.get("context") or {}
|
|
469
|
+
if request.get("sufficient_context") is True or (isinstance(pointers, dict) and pointers.get("sufficient") is True):
|
|
470
|
+
return {"tools": [], "first": None, "reason": "existing context is sufficient"}
|
|
471
|
+
if request.get("exact_text") or request.get("question_kind") == "exact-text":
|
|
472
|
+
return {"tools": ["native"], "first": "native", "reason": "exact-text question"}
|
|
473
|
+
raw_triggers = request.get("triggers", [])
|
|
474
|
+
if isinstance(raw_triggers, str):
|
|
475
|
+
raw_triggers = [raw_triggers]
|
|
476
|
+
trigger_values = {str(item).lower().replace(" ", "_") for item in raw_triggers}
|
|
477
|
+
for key in ARCHITECTURAL_TRIGGERS:
|
|
478
|
+
if request.get(key) is True:
|
|
479
|
+
trigger_values.add(key)
|
|
480
|
+
architectural = bool(trigger_values & ARCHITECTURAL_TRIGGERS)
|
|
481
|
+
phase = str(request.get("phase", "execute")).lower()
|
|
482
|
+
if architectural and phase in {"specify", "design", "review", "deep-review"}:
|
|
483
|
+
return {"tools": ["graphify"], "first": "graphify", "reason": "architectural trigger", "trigger": sorted(trigger_values & ARCHITECTURAL_TRIGGERS)}
|
|
484
|
+
return {"tools": ["graft"], "first": "graft", "reason": "code pointer or relationship is unknown"}
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _validate_metric_record(record: dict[str, Any]) -> None:
|
|
488
|
+
required = {"task_id", "category", "configuration", *CONTROL_FIELDS, "metrics", "gate", "verifier", "outcome"}
|
|
489
|
+
if not isinstance(record, dict) or not record.get("task_id"):
|
|
490
|
+
raise ValueError("benchmark record missing task_id")
|
|
491
|
+
missing = sorted(required - record.keys())
|
|
492
|
+
if missing:
|
|
493
|
+
raise ValueError("benchmark record missing fields: " + ", ".join(missing))
|
|
494
|
+
if record.get("configuration") not in BENCHMARK_CONFIGURATIONS:
|
|
495
|
+
raise ValueError("benchmark record has unsupported configuration")
|
|
496
|
+
if record.get("outcome") not in {"success", "failure", "blocked"}:
|
|
497
|
+
raise ValueError("benchmark record is not terminal")
|
|
498
|
+
if record.get("gate") not in {"PASS", "FAIL", "BLOCKED", "UNAVAILABLE"} or record.get("verifier") not in {"PASS", "FAIL", "BLOCKED", "UNAVAILABLE"}:
|
|
499
|
+
raise ValueError(f"terminal task {record['task_id']} lacks gate/Verifier evidence")
|
|
500
|
+
if record.get("outcome") == "success" and (record["gate"] != "PASS" or record["verifier"] != "PASS"):
|
|
501
|
+
raise ValueError(f"successful task {record['task_id']} lacks gate/Verifier evidence")
|
|
502
|
+
metrics = record.get("metrics")
|
|
503
|
+
if not isinstance(metrics, dict):
|
|
504
|
+
raise ValueError("benchmark record missing metrics")
|
|
505
|
+
for field in METRIC_FIELDS:
|
|
506
|
+
if field not in metrics or not (metrics[field] == "unavailable" or isinstance(metrics[field], int)):
|
|
507
|
+
raise ValueError(f"benchmark metric {field} must be integer or unavailable")
|
|
508
|
+
inputs, outputs, total = (metrics[field] for field in ("input_tokens", "output_tokens", "total_tokens"))
|
|
509
|
+
if all(isinstance(value, int) for value in (inputs, outputs, total)) and total != inputs + outputs:
|
|
510
|
+
raise ValueError("benchmark total_tokens must equal input_tokens + output_tokens")
|
|
511
|
+
if record.get("retention_recommendation") == "remove":
|
|
512
|
+
decision = record.get("project_decision")
|
|
513
|
+
if not isinstance(decision, dict) or decision.get("approved") is not True or not decision.get("id"):
|
|
514
|
+
raise ValueError("removal recommendation requires an explicit project decision")
|
|
515
|
+
if not REMOVAL_SURFACES.issubset(set(decision.get("surfaces", []))):
|
|
516
|
+
raise ValueError("project decision must cover routing, provisioning, configuration, generated_state, and qa_promises")
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def record_benchmark(path: str | Path, record: dict[str, Any]) -> None:
|
|
520
|
+
"""Append one validated terminal record to a checkout-local benchmark ledger."""
|
|
521
|
+
_validate_metric_record(record)
|
|
522
|
+
with Path(path).open("a", encoding="utf-8") as handle:
|
|
523
|
+
handle.write(json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n")
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def benchmark_report(path: str | Path) -> dict[str, Any]:
|
|
527
|
+
records: list[dict[str, Any]] = []
|
|
528
|
+
for line_number, line in enumerate(Path(path).read_text(encoding="utf-8").splitlines(), 1):
|
|
529
|
+
try:
|
|
530
|
+
record = json.loads(line)
|
|
531
|
+
except ValueError as exc:
|
|
532
|
+
raise ValueError(f"malformed benchmark record at line {line_number}") from exc
|
|
533
|
+
_validate_metric_record(record)
|
|
534
|
+
records.append(record)
|
|
535
|
+
by_task: dict[str, list[dict[str, Any]]] = {}
|
|
536
|
+
for record in records:
|
|
537
|
+
by_task.setdefault(record["task_id"], []).append(record)
|
|
538
|
+
if not 10 <= len(by_task) <= 20:
|
|
539
|
+
raise ValueError("benchmark comparison requires 10-20 distinct task IDs with terminal controlled tasks")
|
|
540
|
+
configurations = {record["configuration"] for record in records}
|
|
541
|
+
expected_pair = (
|
|
542
|
+
{"baseline", "graft"} if configurations == {"baseline", "graft"}
|
|
543
|
+
else {"graft", "routed"} if configurations == {"graft", "routed"}
|
|
544
|
+
else None
|
|
545
|
+
)
|
|
546
|
+
if expected_pair is None:
|
|
547
|
+
raise ValueError("benchmark comparison requires baseline->graft or graft->routed pairs")
|
|
548
|
+
mismatches: set[str] = set()
|
|
549
|
+
for task_records in by_task.values():
|
|
550
|
+
baseline = task_records[0]
|
|
551
|
+
task_configurations = {record["configuration"] for record in task_records}
|
|
552
|
+
if task_configurations != expected_pair or len(task_records) != len(expected_pair):
|
|
553
|
+
raise ValueError("benchmark comparison requires matched task/category/configuration pairs")
|
|
554
|
+
if len({record.get("category") for record in task_records}) != 1:
|
|
555
|
+
raise ValueError("benchmark controls mismatch: category")
|
|
556
|
+
for record in task_records[1:]:
|
|
557
|
+
mismatches.update(field for field in CONTROL_FIELDS if record.get(field) != baseline.get(field))
|
|
558
|
+
if mismatches:
|
|
559
|
+
raise ValueError("benchmark controls mismatch: " + ", ".join(sorted(mismatches)))
|
|
560
|
+
def summarize(selected: list[dict[str, Any]]) -> dict[str, Any]:
|
|
561
|
+
return {
|
|
562
|
+
"tasks": len({record["task_id"] for record in selected}),
|
|
563
|
+
"runs": len(selected),
|
|
564
|
+
"outcomes": {outcome: sum(record["outcome"] == outcome for record in selected) for outcome in ("success", "failure", "blocked")},
|
|
565
|
+
"metrics": {
|
|
566
|
+
field: sum(record["metrics"][field] for record in selected if isinstance(record["metrics"][field], int))
|
|
567
|
+
if any(isinstance(record["metrics"][field], int) for record in selected) else "unavailable"
|
|
568
|
+
for field in METRIC_FIELDS
|
|
569
|
+
},
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
grouped: dict[str, dict[str, Any]] = {
|
|
573
|
+
configuration: summarize([record for record in records if record["configuration"] == configuration])
|
|
574
|
+
for configuration in sorted(configurations)
|
|
575
|
+
}
|
|
576
|
+
categories: dict[str, dict[str, Any]] = {}
|
|
577
|
+
for category in sorted({record["category"] for record in records}):
|
|
578
|
+
selected = [record for record in records if record["category"] == category]
|
|
579
|
+
categories[category] = {
|
|
580
|
+
"tasks": len({record["task_id"] for record in selected}),
|
|
581
|
+
"runs": len(selected),
|
|
582
|
+
"configurations": {
|
|
583
|
+
configuration: summarize([record for record in selected if record["configuration"] == configuration])
|
|
584
|
+
for configuration in sorted(configurations)
|
|
585
|
+
},
|
|
586
|
+
}
|
|
587
|
+
recommendations = {record.get("retention_recommendation") for record in records if record.get("retention_recommendation")}
|
|
588
|
+
return {
|
|
589
|
+
"schema": SCHEMA,
|
|
590
|
+
"tasks": len(by_task),
|
|
591
|
+
"runs": len(records),
|
|
592
|
+
"configurations": grouped,
|
|
593
|
+
"categories": categories,
|
|
594
|
+
"controls": list(CONTROL_FIELDS),
|
|
595
|
+
"comparison": "baseline-to-graft" if configurations == {"baseline", "graft"} else "graft-to-routed",
|
|
596
|
+
"removal_decision_required": "remove" in recommendations,
|
|
597
|
+
"project_decisions": [record["project_decision"] for record in records if record.get("project_decision")],
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def agent_metrics(metrics: dict[str, Any]) -> dict[str, Any]:
|
|
602
|
+
"""Expose direct agent activity; indexer internals never count as agent discovery."""
|
|
603
|
+
return {key: value for key, value in metrics.items() if not key.startswith("indexer_") and key not in {"indexer", "indexer_reads", "indexer_calls"}}
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def status(root: Path) -> dict[str, Any]:
|
|
607
|
+
result: dict[str, Any] = {"schema": SCHEMA, "checkout": str(root), "tree": tree_fingerprint(root), "tools": {}}
|
|
608
|
+
for tool, expected in (("graft", GRAFT_VERSION), ("graphify", GRAPHIFY_VERSION)):
|
|
609
|
+
actual, binary = tool_version(root, tool)
|
|
610
|
+
state = read_state(root, tool)
|
|
611
|
+
result["tools"][tool] = {
|
|
612
|
+
"expected_version": expected, "actual_version": actual, "available": binary is not None,
|
|
613
|
+
"status": state.get("status", "degraded") if state else "degraded",
|
|
614
|
+
"backend": state.get("backend", "not-applicable") if state else "not-applicable",
|
|
615
|
+
"tree": state.get("tree") if state else None,
|
|
616
|
+
}
|
|
617
|
+
return result
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _parser() -> argparse.ArgumentParser:
|
|
621
|
+
parser = argparse.ArgumentParser(prog="repository_intelligence.py")
|
|
622
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
623
|
+
state = commands.add_parser("status")
|
|
624
|
+
state.add_argument("--root", required=True)
|
|
625
|
+
state.add_argument("--json", action="store_true")
|
|
626
|
+
for name, operations in (("graft", GRAFT_OPERATIONS), ("graphify", GRAPHIFY_OPERATIONS)):
|
|
627
|
+
command = commands.add_parser(name)
|
|
628
|
+
command.add_argument("--root", required=True)
|
|
629
|
+
command.add_argument("operation", choices=sorted(operations))
|
|
630
|
+
command.add_argument("arguments", nargs=argparse.REMAINDER)
|
|
631
|
+
setup = commands.add_parser("graphify-setup")
|
|
632
|
+
setup.add_argument("--root", required=True)
|
|
633
|
+
setup.add_argument("--backend", required=True, choices=sorted(GRAPHIFY_BACKENDS))
|
|
634
|
+
setup.add_argument("--mode", choices=("code-only", "deep"), default="code-only")
|
|
635
|
+
setup.add_argument("--source-root")
|
|
636
|
+
report = commands.add_parser("benchmark-report")
|
|
637
|
+
report.add_argument("--input", required=True)
|
|
638
|
+
return parser
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
642
|
+
args = _parser().parse_args(argv)
|
|
643
|
+
try:
|
|
644
|
+
if args.command == "benchmark-report":
|
|
645
|
+
output = benchmark_report(args.input)
|
|
646
|
+
else:
|
|
647
|
+
root = checkout_root(args.root)
|
|
648
|
+
if args.command == "status":
|
|
649
|
+
output = status(root)
|
|
650
|
+
elif args.command == "graphify-setup":
|
|
651
|
+
output = graphify_setup(root, args.backend, args.mode, args.source_root)
|
|
652
|
+
else:
|
|
653
|
+
output = _run_context(root, args.command, args.operation, args.arguments)
|
|
654
|
+
print(json.dumps(output, sort_keys=True))
|
|
655
|
+
return DEGRADED_EXIT if output.get("status") == "degraded" else 0
|
|
656
|
+
except IntelligenceError as exc:
|
|
657
|
+
payload = {"schema": SCHEMA, "status": "degraded", "reason": _redact(exc.reason), "fallback": DEGRADED_FALLBACK}
|
|
658
|
+
if exc.expected:
|
|
659
|
+
payload["expected_version"] = exc.expected
|
|
660
|
+
if exc.actual:
|
|
661
|
+
payload["actual_version"] = exc.actual
|
|
662
|
+
print(json.dumps(payload, sort_keys=True))
|
|
663
|
+
print(_redact(exc.reason), file=sys.stderr)
|
|
664
|
+
return DEGRADED_EXIT
|
|
665
|
+
except (ValueError, OSError, subprocess.SubprocessError) as exc:
|
|
666
|
+
print(str(exc), file=sys.stderr)
|
|
667
|
+
return 1
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
if __name__ == "__main__":
|
|
671
|
+
raise SystemExit(main())
|