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.
Files changed (142) hide show
  1. package/.agents/skills/ponytail/SKILL.md +111 -0
  2. package/.agents/skills/ponytail-audit/SKILL.md +37 -0
  3. package/.agents/skills/ponytail-debt/SKILL.md +41 -0
  4. package/.agents/skills/ponytail-gain/SKILL.md +47 -0
  5. package/.agents/skills/ponytail-help/SKILL.md +70 -0
  6. package/.agents/skills/ponytail-review/SKILL.md +52 -0
  7. package/.agents/skills/prompt-review/SKILL.md +60 -0
  8. package/.agents/skills/wtk/SKILL.md +64 -0
  9. package/.agents/skills/wtk/references/artifacts.md +82 -0
  10. package/.agents/skills/wtk/references/evidence.md +68 -0
  11. package/.agents/skills/wtk/references/git.md +75 -0
  12. package/.agents/skills/wtk/references/test-contract.md +76 -0
  13. package/.agents/skills/wtk/references/validation.md +127 -0
  14. package/.agents/skills/wtk-config/SKILL.md +102 -0
  15. package/.agents/skills/wtk-config/assets/agents/claude/deep-reviewer.md +40 -0
  16. package/.agents/skills/wtk-config/assets/agents/claude/designer.md +57 -0
  17. package/.agents/skills/wtk-config/assets/agents/claude/explorer.md +41 -0
  18. package/.agents/skills/wtk-config/assets/agents/claude/implementer.md +57 -0
  19. package/.agents/skills/wtk-config/assets/agents/claude/planner.md +68 -0
  20. package/.agents/skills/wtk-config/assets/agents/claude/verifier.md +80 -0
  21. package/.agents/skills/wtk-config/assets/agents/codex/deep-reviewer.toml +41 -0
  22. package/.agents/skills/wtk-config/assets/agents/codex/designer.toml +55 -0
  23. package/.agents/skills/wtk-config/assets/agents/codex/explorer.toml +35 -0
  24. package/.agents/skills/wtk-config/assets/agents/codex/implementer.toml +52 -0
  25. package/.agents/skills/wtk-config/assets/agents/codex/planner.toml +66 -0
  26. package/.agents/skills/wtk-config/assets/agents/codex/verifier.toml +78 -0
  27. package/.agents/skills/wtk-config/assets/agents/cursor/deep-reviewer.md +38 -0
  28. package/.agents/skills/wtk-config/assets/agents/cursor/designer.md +55 -0
  29. package/.agents/skills/wtk-config/assets/agents/cursor/explorer.md +39 -0
  30. package/.agents/skills/wtk-config/assets/agents/cursor/implementer.md +55 -0
  31. package/.agents/skills/wtk-config/assets/agents/cursor/planner.md +66 -0
  32. package/.agents/skills/wtk-config/assets/agents/cursor/verifier.md +79 -0
  33. package/.agents/skills/wtk-config/scripts/ad-index.py +123 -0
  34. package/.agents/skills/wtk-config/scripts/repository_intelligence.py +671 -0
  35. package/.agents/skills/wtk-config/scripts/workflow_config.py +972 -0
  36. package/.agents/skills/wtk-deep-review/SKILL.md +164 -0
  37. package/.agents/skills/wtk-deep-review/assets/PROMPT.md +56 -0
  38. package/.agents/skills/wtk-deep-review/assets/REVIEW_UI.html +1340 -0
  39. package/.agents/skills/wtk-deep-review/assets/findings.schema.json +113 -0
  40. package/.agents/skills/wtk-deep-review/references/context-pack.md +88 -0
  41. package/.agents/skills/wtk-deep-review/references/orchestration.md +164 -0
  42. package/.agents/skills/wtk-deep-review/references/output-contracts.md +149 -0
  43. package/.agents/skills/wtk-deep-review/references/publish-github.md +83 -0
  44. package/.agents/skills/wtk-deep-review/references/state-and-learnings.md +64 -0
  45. package/.agents/skills/wtk-deep-review/references/subagent-runtimes.md +33 -0
  46. package/.agents/skills/wtk-deep-review/references/taxonomy.md +70 -0
  47. package/.agents/skills/wtk-deep-review/scripts/_common.py +371 -0
  48. package/.agents/skills/wtk-deep-review/scripts/build_jobs.py +561 -0
  49. package/.agents/skills/wtk-deep-review/scripts/build_knowledge.py +349 -0
  50. package/.agents/skills/wtk-deep-review/scripts/build_manifest.py +479 -0
  51. package/.agents/skills/wtk-deep-review/scripts/graft_context.py +160 -0
  52. package/.agents/skills/wtk-deep-review/scripts/graphify_context.py +75 -0
  53. package/.agents/skills/wtk-deep-review/scripts/merge_findings.py +313 -0
  54. package/.agents/skills/wtk-deep-review/scripts/render_html.py +240 -0
  55. package/.agents/skills/wtk-deep-review/scripts/render_review.py +282 -0
  56. package/.agents/skills/wtk-deep-review/scripts/run_jobs.py +436 -0
  57. package/.agents/skills/wtk-deep-review/scripts/token_metrics.py +369 -0
  58. package/.agents/skills/wtk-discover/SKILL.md +270 -0
  59. package/.agents/skills/wtk-discover/references/document-format.md +149 -0
  60. package/.agents/skills/wtk-implement/SKILL.md +95 -0
  61. package/.agents/skills/wtk-implement/references/checklist-format.md +116 -0
  62. package/.agents/skills/wtk-implement/references/screens.md +129 -0
  63. package/.agents/skills/wtk-implement/references/test-policy.md +152 -0
  64. package/.agents/skills/wtk-implement/references/verify.md +297 -0
  65. package/.agents/skills/wtk-knowledge-check/SKILL.md +16 -0
  66. package/.agents/skills/wtk-knowledge-check/scripts/check.ts +502 -0
  67. package/.agents/skills/wtk-knowledge-check/scripts/cli.ts +32 -0
  68. package/.agents/skills/wtk-knowledge-check/scripts/frontmatter.ts +44 -0
  69. package/.agents/skills/wtk-lean/.skill-meta.json +6 -0
  70. package/.agents/skills/wtk-lean/NOTICE.md +18 -0
  71. package/.agents/skills/wtk-lean/SKILL.md +137 -0
  72. package/.agents/skills/wtk-lean/references/build.md +168 -0
  73. package/.agents/skills/wtk-lean/references/checks.md +253 -0
  74. package/.agents/skills/wtk-lean/references/memory.md +156 -0
  75. package/.agents/skills/wtk-lean/references/plan.md +514 -0
  76. package/.agents/skills/wtk-lean/references/verify.md +336 -0
  77. package/.agents/skills/wtk-lean/scripts/check_commit.py +121 -0
  78. package/.agents/skills/wtk-lean/scripts/fixtures/checks.md +98 -0
  79. package/.agents/skills/wtk-lean/scripts/fixtures/plan.md +119 -0
  80. package/.agents/skills/wtk-lean/scripts/fixtures/verification.md +41 -0
  81. package/.agents/skills/wtk-lean/scripts/lessons.py +412 -0
  82. package/.agents/skills/wtk-lean/scripts/selftest.py +401 -0
  83. package/.agents/skills/wtk-lean/scripts/validate_checks.py +417 -0
  84. package/.agents/skills/wtk-lean/scripts/validate_plan.py +580 -0
  85. package/.agents/skills/wtk-lean/scripts/validate_verification.py +357 -0
  86. package/.agents/skills/wtk-plan/SKILL.md +102 -0
  87. package/.agents/skills/wtk-plan/references/document-format.md +222 -0
  88. package/.agents/skills/wtk-qa/SKILL.md +14 -0
  89. package/.agents/skills/wtk-qa-execute/SKILL.md +115 -0
  90. package/.agents/skills/wtk-qa-execute/references/fix-loop.md +23 -0
  91. package/.agents/skills/wtk-qa-execute/references/session-protocol.md +25 -0
  92. package/.agents/skills/wtk-qa-plan/SKILL.md +111 -0
  93. package/.agents/skills/wtk-qa-plan/references/profile.md +35 -0
  94. package/.agents/skills/wtk-ship/SKILL.md +49 -0
  95. package/.agents/skills/wtk-ship/remediation.py +170 -0
  96. package/.agents/skills/wtk-ship/scripts/close_feature.py +83 -0
  97. package/.agents/skills/wtk-ship/scripts/review_convergence.py +373 -0
  98. package/.wtk.toml.example +93 -0
  99. package/AGENTS.md +129 -0
  100. package/NOTICE.md +12 -0
  101. package/README.md +440 -0
  102. package/bin/wtk.js +25 -0
  103. package/docs/toolkit/README.md +56 -0
  104. package/docs/toolkit/decisions.md +61 -0
  105. package/docs/toolkit/guidelines/CONTEXT-BUDGET.md +64 -0
  106. package/docs/toolkit/guidelines/DX.md +67 -0
  107. package/docs/toolkit/guidelines/FRONTEND.md +73 -0
  108. package/docs/toolkit/guidelines/KNOWLEDGE-WIKI.md +128 -0
  109. package/docs/toolkit/guidelines/MODELING.md +134 -0
  110. package/docs/toolkit/guidelines/QA-EXECUTION.md +46 -0
  111. package/docs/toolkit/guidelines/QA-SCENARIOS.md +165 -0
  112. package/docs/toolkit/guidelines/REVIEW-ROUNDS.md +160 -0
  113. package/docs/toolkit/guidelines/SECURITY.md +160 -0
  114. package/docs/toolkit/guidelines/UI-UX.md +117 -0
  115. package/docs/toolkit/guidelines/WORKFLOW-MEMORY.md +73 -0
  116. package/docs/toolkit/guidelines.md +50 -0
  117. package/docs/toolkit/loop.md +77 -0
  118. package/docs/toolkit/purpose.md +51 -0
  119. package/docs/toolkit/repository-intelligence.md +53 -0
  120. package/docs/toolkit/reviews.md +77 -0
  121. package/knowledge/AGENTS.md +316 -0
  122. package/knowledge/raw/README.md +17 -0
  123. package/package.json +74 -0
  124. package/scripts/install_security_skills.py +816 -0
  125. package/scripts/installer/engine.js +254 -0
  126. package/scripts/installer/knowledge.js +28 -0
  127. package/scripts/installer/packets.js +77 -0
  128. package/scripts/installer/terminal.js +98 -0
  129. package/scripts/installer/transaction.js +75 -0
  130. package/skills-lock.json +92 -0
  131. package/templates/adoption/agents/core.md +23 -0
  132. package/templates/adoption/agents/quality.md +3 -0
  133. package/templates/adoption/knowledge/wiki/architecture/index.md +3 -0
  134. package/templates/adoption/knowledge/wiki/decisions/index.md +3 -0
  135. package/templates/adoption/knowledge/wiki/design/index.md +3 -0
  136. package/templates/adoption/knowledge/wiki/domain/index.md +3 -0
  137. package/templates/adoption/knowledge/wiki/index.md +3 -0
  138. package/templates/adoption/knowledge/wiki/log.md +3 -0
  139. package/templates/adoption/knowledge/wiki/open-questions/index.md +3 -0
  140. package/templates/adoption/knowledge/wiki/product/index.md +3 -0
  141. package/templates/adoption/knowledge/wiki/research/index.md +3 -0
  142. 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())