rdap-q 1.2.1 → 1.2.4
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/plugins/rdap-q/plugin.json +1 -1
- package/.agents/skills/rdap-q/README.md +1 -1
- package/.agents/skills/rdap-q/SKILL.md +1 -1
- package/.agents/skills/rdap-q/install/install-unix.sh +1 -1
- package/.agents/skills/rdap-q/manifest.json +1 -1
- package/.claude/commands/rdapq.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin-index.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.clinerules +1 -1
- package/.codex/skill.json +1 -1
- package/.github/copilot-instructions.md +1 -1
- package/.goosehints +1 -1
- package/.grok/rules.md +1 -1
- package/.grok/skill.json +1 -1
- package/.grok-plugin/marketplace.json +1 -1
- package/.grok-plugin/plugin-index.json +1 -1
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/GEMINI.md +1 -1
- package/README.md +90 -224
- package/agentskills.json +1 -1
- package/install.ps1 +2 -2
- package/install.sh +2 -2
- package/lib/installer.js +43 -27
- package/manifest.json +1 -1
- package/marketplace.json +1 -1
- package/package.json +3 -2
- package/plugin.json +1 -1
- package/plugins/rdap-q/plugin.json +1 -1
- package/plugins/rdap-q/skills/rdap-q/README.md +1 -1
- package/plugins/rdap-q/skills/rdap-q/SKILL.md +1 -1
- package/plugins/rdap-q/skills/rdap-q/install/install-unix.sh +1 -1
- package/plugins/rdap-q/skills/rdap-q/manifest.json +1 -1
- package/rdap-q-skill/README.md +1 -1
- package/rdap-q-skill/SKILL.md +1 -1
- package/rdap-q-skill/install/install-unix.sh +1 -1
- package/rdap-q-skill/manifest.json +1 -1
- package/scripts/harness-audit/check.py +387 -0
- package/scripts/harness-audit/vendors.json +192 -0
- package/scripts/harness-audit/weekly.sh +22 -0
- package/skills.json +1 -1
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Compare RDAP-Q install paths with current vendor documentation.
|
|
3
|
+
|
|
4
|
+
Stdlib only. A green run does not call a model. On drift it writes a review
|
|
5
|
+
prompt that a local model can read. Exit 0 when every claim still matches,
|
|
6
|
+
1 when a path disappeared from the installer, the README, or the vendor docs,
|
|
7
|
+
2 when a harness could not be checked because every fetch failed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import hashlib
|
|
14
|
+
import ipaddress
|
|
15
|
+
import json
|
|
16
|
+
import socket
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
import urllib.error
|
|
20
|
+
import urllib.request
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from urllib.parse import urljoin, urlparse
|
|
24
|
+
|
|
25
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
26
|
+
DEFAULT_VENDORS = Path(__file__).resolve().parent / "vendors.json"
|
|
27
|
+
USER_AGENT = "rdapq-harness-audit/1"
|
|
28
|
+
MAX_BYTES = 1_500_000
|
|
29
|
+
TIMEOUT = 25
|
|
30
|
+
REDIRECTS = (301, 302, 303, 307, 308)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RefuseAutoRedirect(urllib.request.HTTPRedirectHandler):
|
|
34
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
35
|
+
raise urllib.error.HTTPError(req.full_url, code, msg, headers, fp)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
OPENER = urllib.request.build_opener(RefuseAutoRedirect)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def url_refusal(url: str) -> str | None:
|
|
42
|
+
"""Refuse anything a weekly cron on a home server should not request."""
|
|
43
|
+
parsed = urlparse(url)
|
|
44
|
+
if parsed.scheme != "https" or parsed.username or parsed.password:
|
|
45
|
+
return "only https URLs without credentials are fetched"
|
|
46
|
+
host = (parsed.hostname or "").lower().rstrip(".")
|
|
47
|
+
if not host:
|
|
48
|
+
return "missing host"
|
|
49
|
+
if host == "localhost" or host.endswith(".local") or host.endswith(".internal"):
|
|
50
|
+
return "local hostname refused"
|
|
51
|
+
try:
|
|
52
|
+
literal = ipaddress.ip_address(host)
|
|
53
|
+
addresses = [literal]
|
|
54
|
+
except ValueError:
|
|
55
|
+
try:
|
|
56
|
+
answers = socket.getaddrinfo(host, 443, type=socket.SOCK_STREAM)
|
|
57
|
+
except socket.gaierror as error:
|
|
58
|
+
return f"dns failed: {error}"
|
|
59
|
+
addresses = []
|
|
60
|
+
for answer in answers:
|
|
61
|
+
try:
|
|
62
|
+
addresses.append(ipaddress.ip_address(answer[4][0]))
|
|
63
|
+
except ValueError:
|
|
64
|
+
return "unreadable address"
|
|
65
|
+
for ip in addresses:
|
|
66
|
+
if any([
|
|
67
|
+
ip.is_private,
|
|
68
|
+
ip.is_loopback,
|
|
69
|
+
ip.is_link_local,
|
|
70
|
+
ip.is_reserved,
|
|
71
|
+
ip.is_multicast,
|
|
72
|
+
ip.is_unspecified,
|
|
73
|
+
]):
|
|
74
|
+
return f"refusing non-public address {ip}"
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def fetch(url: str) -> tuple[str, str, str]:
|
|
79
|
+
current = url
|
|
80
|
+
last_error = "no response"
|
|
81
|
+
for _ in range(6):
|
|
82
|
+
refusal = url_refusal(current)
|
|
83
|
+
if refusal:
|
|
84
|
+
return current, refusal, ""
|
|
85
|
+
req = urllib.request.Request(current, headers={"User-Agent": USER_AGENT})
|
|
86
|
+
try:
|
|
87
|
+
with OPENER.open(req, timeout=TIMEOUT) as response:
|
|
88
|
+
final = response.geturl()
|
|
89
|
+
final_refusal = url_refusal(final)
|
|
90
|
+
if final_refusal:
|
|
91
|
+
return final, final_refusal, ""
|
|
92
|
+
body = response.read(MAX_BYTES + 1)
|
|
93
|
+
if len(body) > MAX_BYTES:
|
|
94
|
+
body = body[:MAX_BYTES]
|
|
95
|
+
return final, str(response.status), body.decode("utf-8", "replace")
|
|
96
|
+
except urllib.error.HTTPError as error:
|
|
97
|
+
location = error.headers.get("Location") if error.headers else None
|
|
98
|
+
if error.code in REDIRECTS and location:
|
|
99
|
+
current = urljoin(current, location)
|
|
100
|
+
last_error = f"HTTP {error.code}"
|
|
101
|
+
continue
|
|
102
|
+
return current, f"HTTP {error.code}", ""
|
|
103
|
+
except Exception as error: # noqa: BLE001 - report the network failure, do not crash the sweep
|
|
104
|
+
last_error = f"{type(error).__name__}: {error}"
|
|
105
|
+
break
|
|
106
|
+
return current, last_error, ""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def load_cache(path: Path, max_age: int, refresh: bool) -> dict | None:
|
|
110
|
+
if refresh or not path.exists():
|
|
111
|
+
return None
|
|
112
|
+
try:
|
|
113
|
+
cached = json.loads(path.read_text())
|
|
114
|
+
except (OSError, json.JSONDecodeError):
|
|
115
|
+
return None
|
|
116
|
+
age = time.time() - float(cached.get("fetched_at", 0))
|
|
117
|
+
if age > max_age:
|
|
118
|
+
return None
|
|
119
|
+
body_path = Path(cached.get("body", ""))
|
|
120
|
+
try:
|
|
121
|
+
body_path = body_path.resolve()
|
|
122
|
+
cache_root = path.parent.resolve()
|
|
123
|
+
except OSError:
|
|
124
|
+
return None
|
|
125
|
+
if cache_root not in body_path.parents:
|
|
126
|
+
return None
|
|
127
|
+
if not body_path.is_file():
|
|
128
|
+
return None
|
|
129
|
+
cached["text"] = body_path.read_text(encoding="utf-8", errors="replace")
|
|
130
|
+
return cached
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def store_cache(path: Path, url: str, final: str, status: str, text: str) -> None:
|
|
134
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
body = path.with_suffix(".body")
|
|
136
|
+
body.write_text(text, encoding="utf-8")
|
|
137
|
+
path.write_text(json.dumps({
|
|
138
|
+
"url": url,
|
|
139
|
+
"final": final,
|
|
140
|
+
"status": status,
|
|
141
|
+
"fetched_at": time.time(),
|
|
142
|
+
"body": str(body),
|
|
143
|
+
}))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def cached_fetch(url: str, cache_dir: Path, max_age: int, refresh: bool) -> dict:
|
|
147
|
+
key = hashlib.sha256(url.encode("utf-8")).hexdigest()
|
|
148
|
+
slot = cache_dir / f"{key}.json"
|
|
149
|
+
cached = load_cache(slot, max_age, refresh)
|
|
150
|
+
if cached:
|
|
151
|
+
cached["cache"] = "hit"
|
|
152
|
+
return cached
|
|
153
|
+
final, status, text = fetch(url)
|
|
154
|
+
if text:
|
|
155
|
+
store_cache(slot, url, final, status, text)
|
|
156
|
+
return {"url": url, "final": final, "status": status, "text": text, "cache": "miss"}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def links_in(text: str) -> list[str]:
|
|
160
|
+
found = []
|
|
161
|
+
token = []
|
|
162
|
+
for char in text:
|
|
163
|
+
if char in " \t\r\n<>\"'()[]":
|
|
164
|
+
word = "".join(token)
|
|
165
|
+
if word.startswith("https://"):
|
|
166
|
+
found.append(word.rstrip(".,"))
|
|
167
|
+
token = []
|
|
168
|
+
continue
|
|
169
|
+
token.append(char)
|
|
170
|
+
if token:
|
|
171
|
+
word = "".join(token)
|
|
172
|
+
if word.startswith("https://"):
|
|
173
|
+
found.append(word.rstrip(".,"))
|
|
174
|
+
return found
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def host_ok(url: str, index_host: str, allow: list[str]) -> bool:
|
|
178
|
+
host = urlparse(url).hostname or ""
|
|
179
|
+
allowed = {index_host, *allow}
|
|
180
|
+
return host in allowed
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def corpus_for(harness: dict, cache_dir: Path, max_age: int, refresh: bool) -> tuple[str, list[dict]]:
|
|
184
|
+
pages = []
|
|
185
|
+
seen = set()
|
|
186
|
+
|
|
187
|
+
def add(url: str) -> None:
|
|
188
|
+
if not url or url in seen:
|
|
189
|
+
return
|
|
190
|
+
seen.add(url)
|
|
191
|
+
pages.append(cached_fetch(url, cache_dir, max_age, refresh))
|
|
192
|
+
|
|
193
|
+
for url in harness.get("indexes", []):
|
|
194
|
+
add(url)
|
|
195
|
+
for url in harness.get("pins", []):
|
|
196
|
+
add(url)
|
|
197
|
+
|
|
198
|
+
index_text = ""
|
|
199
|
+
index_host = ""
|
|
200
|
+
for page in pages:
|
|
201
|
+
if page["url"] in harness.get("indexes", []) and page.get("text"):
|
|
202
|
+
index_text += "\n" + page["text"]
|
|
203
|
+
index_host = urlparse(page.get("final") or page["url"]).hostname or ""
|
|
204
|
+
allow = harness.get("allow_hosts", [])
|
|
205
|
+
patterns = [item.lower() for item in harness.get("follow", [])]
|
|
206
|
+
followed = 0
|
|
207
|
+
limit = int(harness.get("follow_limit", 6))
|
|
208
|
+
for link in links_in(index_text):
|
|
209
|
+
if followed >= limit:
|
|
210
|
+
break
|
|
211
|
+
if not any(pattern in link.lower() for pattern in patterns):
|
|
212
|
+
continue
|
|
213
|
+
if index_host and not host_ok(link, index_host, allow):
|
|
214
|
+
continue
|
|
215
|
+
if link in seen:
|
|
216
|
+
continue
|
|
217
|
+
add(link)
|
|
218
|
+
followed += 1
|
|
219
|
+
|
|
220
|
+
usable = [page for page in pages if page.get("text")]
|
|
221
|
+
return "\n".join(page["text"] for page in usable), pages
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def evaluate(spec: dict, installer: str, readme: str, cache_dir: Path, max_age: int, refresh: bool) -> dict:
|
|
225
|
+
results = []
|
|
226
|
+
for harness in spec["harnesses"]:
|
|
227
|
+
text, pages = corpus_for(harness, cache_dir, max_age, refresh)
|
|
228
|
+
fetched = [page for page in pages if page.get("text")]
|
|
229
|
+
for claim in harness["claims"]:
|
|
230
|
+
results.append({
|
|
231
|
+
"harness": harness["id"],
|
|
232
|
+
"claim": claim["id"],
|
|
233
|
+
"installer": claim["installer"] in installer,
|
|
234
|
+
"docs": claim["docs"] in text,
|
|
235
|
+
"readme": claim["readme"] in readme,
|
|
236
|
+
"sources": len(fetched),
|
|
237
|
+
"pages": [page.get("final") or page["url"] for page in fetched],
|
|
238
|
+
"errors": [f"{page['url']} ({page['status']})" for page in pages if not page.get("text")],
|
|
239
|
+
})
|
|
240
|
+
forbidden = []
|
|
241
|
+
for item in spec.get("forbidden_in_readme", []):
|
|
242
|
+
lines = [line for line in readme.splitlines() if item in line]
|
|
243
|
+
if any("do not" not in line.lower() for line in lines):
|
|
244
|
+
forbidden.append(item)
|
|
245
|
+
return {"results": results, "forbidden": forbidden}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def status_of(row: dict) -> str:
|
|
249
|
+
if row["sources"] == 0:
|
|
250
|
+
return "UNCHECKED"
|
|
251
|
+
if row["installer"] and row["docs"] and row["readme"]:
|
|
252
|
+
return "OK"
|
|
253
|
+
return "DRIFT"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def render(report: dict) -> str:
|
|
257
|
+
lines = [
|
|
258
|
+
"# RDAP-Q harness audit",
|
|
259
|
+
"",
|
|
260
|
+
f"Checked: {report['checked_at']}",
|
|
261
|
+
"",
|
|
262
|
+
"| Harness | Claim | Installer | Docs | README | Status |",
|
|
263
|
+
"|---|---|---|---|---|---|",
|
|
264
|
+
]
|
|
265
|
+
for row in report["results"]:
|
|
266
|
+
state = status_of(row)
|
|
267
|
+
mark = lambda ok: "yes" if ok else "NO"
|
|
268
|
+
lines.append(
|
|
269
|
+
f"| {row['harness']} | {row['claim']} | {mark(row['installer'])} | "
|
|
270
|
+
f"{mark(row['docs'])} | {mark(row['readme'])} | {state} |"
|
|
271
|
+
)
|
|
272
|
+
if report["forbidden"]:
|
|
273
|
+
lines.extend(["", "## README still recommends a removed command", ""])
|
|
274
|
+
lines.extend(f"- `{item}`" for item in report["forbidden"])
|
|
275
|
+
problems = [row for row in report["results"] if status_of(row) != "OK"]
|
|
276
|
+
if problems:
|
|
277
|
+
lines.extend(["", "## What failed", ""])
|
|
278
|
+
for row in problems:
|
|
279
|
+
lines.append(f"### {row['harness']} / {row['claim']}")
|
|
280
|
+
lines.append("")
|
|
281
|
+
lines.append(f"- sources fetched: {row['sources']}")
|
|
282
|
+
if row["errors"]:
|
|
283
|
+
lines.append("- fetch errors: " + "; ".join(row["errors"]))
|
|
284
|
+
if row["pages"]:
|
|
285
|
+
lines.append("- pages: " + ", ".join(row["pages"]))
|
|
286
|
+
lines.append("")
|
|
287
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def review_prompt(report: dict) -> str:
|
|
291
|
+
failed = [row for row in report["results"] if status_of(row) != "OK"]
|
|
292
|
+
lines = [
|
|
293
|
+
"The RDAP-Q harness audit found drift. Do not change install paths unless the fetched vendor page contradicts lib/installer.js.",
|
|
294
|
+
"Read the cached page bodies listed below and say which claim is stale.",
|
|
295
|
+
"",
|
|
296
|
+
]
|
|
297
|
+
for row in failed:
|
|
298
|
+
lines.append(f"- {row['harness']} {row['claim']}: installer={row['installer']} docs={row['docs']} readme={row['readme']}")
|
|
299
|
+
lines.extend(f" page: {page}" for page in row["pages"])
|
|
300
|
+
lines.extend(f" error: {error}" for error in row["errors"])
|
|
301
|
+
if report["forbidden"]:
|
|
302
|
+
lines.append("README contains removed commands: " + ", ".join(report["forbidden"]))
|
|
303
|
+
lines.append("")
|
|
304
|
+
lines.append("Reply with the file and path that should change, or say the vendor page was unreachable.")
|
|
305
|
+
return "\n".join(lines) + "\n"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def exit_code(report: dict) -> int:
|
|
309
|
+
rows = report["results"]
|
|
310
|
+
if report["forbidden"] or any(status_of(row) == "DRIFT" for row in rows):
|
|
311
|
+
return 1
|
|
312
|
+
if any(status_of(row) == "UNCHECKED" for row in rows):
|
|
313
|
+
return 2
|
|
314
|
+
return 0
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def self_test() -> int:
|
|
318
|
+
spec = {
|
|
319
|
+
"forbidden_in_readme": ["goose toolkit add"],
|
|
320
|
+
"harnesses": [{
|
|
321
|
+
"id": "demo",
|
|
322
|
+
"indexes": [],
|
|
323
|
+
"pins": [],
|
|
324
|
+
"follow": [],
|
|
325
|
+
"claims": [{
|
|
326
|
+
"id": "skill",
|
|
327
|
+
"installer": "path.join(home, '.cursor')",
|
|
328
|
+
"docs": ".cursor/skills",
|
|
329
|
+
"readme": "~/.cursor/skills/rdap-q/",
|
|
330
|
+
}],
|
|
331
|
+
}],
|
|
332
|
+
}
|
|
333
|
+
text = "see .cursor/skills for skills"
|
|
334
|
+
report = evaluate(spec, "path.join(home, '.cursor')", "~/.cursor/skills/rdap-q/", Path("/tmp"), 0, True)
|
|
335
|
+
# evaluate fetches pins; with no URLs the corpus is empty, so docs fails.
|
|
336
|
+
row = report["results"][0]
|
|
337
|
+
assert row["installer"] and row["readme"] and not row["docs"]
|
|
338
|
+
assert status_of(row) == "UNCHECKED"
|
|
339
|
+
assert links_in("see https://example.com/skills.md now") == ["https://example.com/skills.md"]
|
|
340
|
+
assert url_refusal("http://example.com/a") == "only https URLs without credentials are fetched"
|
|
341
|
+
assert url_refusal("https://user:pass@example.com/a") == "only https URLs without credentials are fetched"
|
|
342
|
+
assert url_refusal("https://127.0.0.1/a") == "refusing non-public address 127.0.0.1"
|
|
343
|
+
assert url_refusal("https://10.1.1.1/a") == "refusing non-public address 10.1.1.1"
|
|
344
|
+
assert url_refusal("https://athena.local/a") == "local hostname refused"
|
|
345
|
+
bad = evaluate(spec, "nope", "goose toolkit add", Path("/tmp"), 0, True)
|
|
346
|
+
assert bad["forbidden"] == ["goose toolkit add"]
|
|
347
|
+
assert exit_code(bad) == 1
|
|
348
|
+
print("self-test ok")
|
|
349
|
+
return 0
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def main(argv: list[str]) -> int:
|
|
353
|
+
parser = argparse.ArgumentParser(description="Check RDAP-Q install paths against vendor docs")
|
|
354
|
+
parser.add_argument("--vendors", type=Path, default=DEFAULT_VENDORS)
|
|
355
|
+
parser.add_argument("--report", type=Path, default=ROOT / "reports" / "harness-audit" / "latest.md")
|
|
356
|
+
parser.add_argument("--prompt", type=Path, default=ROOT / "reports" / "harness-audit" / "review-prompt.md")
|
|
357
|
+
parser.add_argument("--cache", type=Path, default=None)
|
|
358
|
+
parser.add_argument("--max-age-hours", type=float, default=144)
|
|
359
|
+
parser.add_argument("--refresh", action="store_true")
|
|
360
|
+
parser.add_argument("--self-test", action="store_true")
|
|
361
|
+
args = parser.parse_args(argv)
|
|
362
|
+
if args.self_test:
|
|
363
|
+
return self_test()
|
|
364
|
+
|
|
365
|
+
spec = json.loads(args.vendors.read_text())
|
|
366
|
+
installer = (ROOT / spec["installer"]).read_text()
|
|
367
|
+
readme = (ROOT / spec["readme"]).read_text()
|
|
368
|
+
cache_dir = args.cache or args.report.parent / "cache"
|
|
369
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
370
|
+
report = evaluate(spec, installer, readme, cache_dir, int(args.max_age_hours * 3600), args.refresh)
|
|
371
|
+
report["checked_at"] = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
372
|
+
body = render(report)
|
|
373
|
+
args.report.parent.mkdir(parents=True, exist_ok=True)
|
|
374
|
+
args.report.write_text(body)
|
|
375
|
+
code = exit_code(report)
|
|
376
|
+
if code == 0:
|
|
377
|
+
if args.prompt.exists():
|
|
378
|
+
args.prompt.unlink()
|
|
379
|
+
else:
|
|
380
|
+
args.prompt.write_text(review_prompt(report))
|
|
381
|
+
sys.stdout.write(body)
|
|
382
|
+
print(f"exit {code}")
|
|
383
|
+
return code
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
if __name__ == "__main__":
|
|
387
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
{
|
|
2
|
+
"installer": "lib/installer.js",
|
|
3
|
+
"readme": "README.md",
|
|
4
|
+
"forbidden_in_readme": [
|
|
5
|
+
"gh extension install coldcanuk/rdapq",
|
|
6
|
+
"goose toolkit add",
|
|
7
|
+
"codex skill add",
|
|
8
|
+
"agy plugin marketplace add"
|
|
9
|
+
],
|
|
10
|
+
"harnesses": [
|
|
11
|
+
{
|
|
12
|
+
"id": "cursor",
|
|
13
|
+
"indexes": ["https://cursor.com/llms.txt"],
|
|
14
|
+
"pins": [
|
|
15
|
+
"https://cursor.com/docs/skills.md",
|
|
16
|
+
"https://cursor.com/docs/rules.md"
|
|
17
|
+
],
|
|
18
|
+
"follow": ["skills.md", "rules.md"],
|
|
19
|
+
"claims": [
|
|
20
|
+
{
|
|
21
|
+
"id": "user-skill",
|
|
22
|
+
"installer": "path.join(home, '.cursor')",
|
|
23
|
+
"docs": ".cursor/skills",
|
|
24
|
+
"readme": "~/.cursor/skills/rdap-q/"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "project-skill",
|
|
28
|
+
"installer": "path.join(layout.agents, 'skills', 'rdap-q')",
|
|
29
|
+
"docs": ".agents/skills",
|
|
30
|
+
"readme": ".agents/skills/rdap-q/"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "codex",
|
|
36
|
+
"indexes": ["https://developers.openai.com/llms.txt"],
|
|
37
|
+
"pins": [
|
|
38
|
+
"https://learn.chatgpt.com/docs/build-skills.md",
|
|
39
|
+
"https://learn.chatgpt.com/docs/agent-configuration/agents-md",
|
|
40
|
+
"https://developers.openai.com/codex/skills.md"
|
|
41
|
+
],
|
|
42
|
+
"follow": ["codex", "agents-md", "skill"],
|
|
43
|
+
"allow_hosts": ["learn.chatgpt.com", "developers.openai.com"],
|
|
44
|
+
"claims": [
|
|
45
|
+
{
|
|
46
|
+
"id": "user-agents",
|
|
47
|
+
"installer": "path.join(layout.codex, 'AGENTS.md')",
|
|
48
|
+
"docs": ".codex",
|
|
49
|
+
"readme": "~/.codex/AGENTS.md"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "user-skill",
|
|
53
|
+
"installer": "path.join(layout.agents, 'skills', 'rdap-q')",
|
|
54
|
+
"docs": ".agents/skills",
|
|
55
|
+
"readme": "~/.agents/skills/rdap-q/"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "claude",
|
|
61
|
+
"indexes": ["https://code.claude.com/docs/llms.txt"],
|
|
62
|
+
"pins": ["https://code.claude.com/docs/en/skills.md"],
|
|
63
|
+
"follow": ["/skills.md"],
|
|
64
|
+
"claims": [
|
|
65
|
+
{
|
|
66
|
+
"id": "user-skill",
|
|
67
|
+
"installer": "path.join(layout.claude, 'skills', 'rdap-q')",
|
|
68
|
+
"docs": ".claude/skills",
|
|
69
|
+
"readme": "~/.claude/skills/rdap-q/"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "slash-command",
|
|
73
|
+
"installer": "path.join(layout.claude, 'commands', 'rdapq.md')",
|
|
74
|
+
"docs": ".claude/commands",
|
|
75
|
+
"readme": "~/.claude/commands/rdapq.md"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "copilot",
|
|
81
|
+
"indexes": ["https://docs.github.com/llms.txt"],
|
|
82
|
+
"pins": [
|
|
83
|
+
"https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-custom-instructions.md",
|
|
84
|
+
"https://docs.github.com/copilot/reference/customization-cheat-sheet.md"
|
|
85
|
+
],
|
|
86
|
+
"follow": ["copilot-instructions", "customization-cheat-sheet", "custom-instructions"],
|
|
87
|
+
"claims": [
|
|
88
|
+
{
|
|
89
|
+
"id": "user-instructions",
|
|
90
|
+
"installer": "path.join(home, '.copilot')",
|
|
91
|
+
"docs": ".copilot",
|
|
92
|
+
"readme": "~/.copilot/copilot-instructions.md"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": "repo-instructions",
|
|
96
|
+
"installer": "copilot-instructions.md",
|
|
97
|
+
"docs": "copilot-instructions.md",
|
|
98
|
+
"readme": ".github/copilot-instructions.md"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "user-skill",
|
|
102
|
+
"installer": "path.join(layout.copilot, 'skills', 'rdap-q')",
|
|
103
|
+
"docs": ".github/skills",
|
|
104
|
+
"readme": "~/.copilot/skills/rdap-q/"
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"id": "grok",
|
|
110
|
+
"indexes": ["https://docs.x.ai/llms.txt"],
|
|
111
|
+
"pins": ["https://docs.x.ai/build/features/skills-plugins-marketplaces.md"],
|
|
112
|
+
"follow": ["skills-plugins", "project-rules"],
|
|
113
|
+
"claims": [
|
|
114
|
+
{
|
|
115
|
+
"id": "user-skill",
|
|
116
|
+
"installer": "path.join(layout.grok, 'skills', 'rdap-q')",
|
|
117
|
+
"docs": ".grok/skills",
|
|
118
|
+
"readme": "~/.grok/skills/rdap-q/"
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"id": "antigravity",
|
|
124
|
+
"indexes": ["https://antigravity.google/llms.txt"],
|
|
125
|
+
"pins": [
|
|
126
|
+
"https://antigravity.google/docs/skills.md",
|
|
127
|
+
"https://antigravity.google/docs/skills"
|
|
128
|
+
],
|
|
129
|
+
"follow": ["/docs/skills", "gcli-migration", "plugins"],
|
|
130
|
+
"claims": [
|
|
131
|
+
{
|
|
132
|
+
"id": "cli-skill",
|
|
133
|
+
"installer": "path.join(layout.gemini, 'antigravity-cli', 'skills', 'rdap-q')",
|
|
134
|
+
"docs": "antigravity-cli",
|
|
135
|
+
"readme": "~/.gemini/antigravity-cli/skills/rdap-q/"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"id": "ide-skill",
|
|
139
|
+
"installer": "path.join(layout.gemini, 'config', 'skills', 'rdap-q')",
|
|
140
|
+
"docs": "config/skills",
|
|
141
|
+
"readme": "~/.gemini/config/skills/rdap-q/"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"id": "project-skill",
|
|
145
|
+
"installer": "path.join(abs, '.agents', 'skills', 'rdap-q')",
|
|
146
|
+
"docs": ".agents/skills",
|
|
147
|
+
"readme": ".agents/skills/rdap-q/"
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"id": "goose",
|
|
153
|
+
"indexes": ["https://goose-docs.ai/llms.txt"],
|
|
154
|
+
"pins": ["https://goose-docs.ai/docs/guides/context-engineering/using-goosehints"],
|
|
155
|
+
"follow": ["goosehints", "hint"],
|
|
156
|
+
"claims": [
|
|
157
|
+
{
|
|
158
|
+
"id": "global-hints",
|
|
159
|
+
"installer": "path.join(layout.goose, '.goosehints')",
|
|
160
|
+
"docs": ".goosehints",
|
|
161
|
+
"readme": "~/.config/goose/.goosehints"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"id": "home",
|
|
165
|
+
"installer": "path.join(home, '.config', 'goose')",
|
|
166
|
+
"docs": ".config/goose",
|
|
167
|
+
"readme": "~/.config/goose/.goosehints"
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"id": "cline",
|
|
173
|
+
"indexes": ["https://docs.cline.bot/llms.txt"],
|
|
174
|
+
"pins": ["https://docs.cline.bot/customization/cline-rules.md"],
|
|
175
|
+
"follow": ["cline-rules"],
|
|
176
|
+
"claims": [
|
|
177
|
+
{
|
|
178
|
+
"id": "user-rules",
|
|
179
|
+
"installer": "path.join(layout.cline, 'rules', 'rdapq.md')",
|
|
180
|
+
"docs": ".cline/rules",
|
|
181
|
+
"readme": "~/.cline/rules/rdapq.md"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"id": "project-rules",
|
|
185
|
+
"installer": "path.join('.clinerules', 'rdapq.md')",
|
|
186
|
+
"docs": ".clinerules",
|
|
187
|
+
"readme": ".clinerules/rdapq.md"
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Weekly vendor check. Cron this on a machine that can reach the public docs.
|
|
3
|
+
# It does not call a model. On drift it writes reports/harness-audit/review-prompt.md
|
|
4
|
+
# and exits non-zero. It does not execute a review command from the environment.
|
|
5
|
+
#
|
|
6
|
+
# crontab, from the repository root, as the user who owns /opt/repo/rdapq:
|
|
7
|
+
# 10 9 * * 1 cd /opt/repo/rdapq && ./scripts/harness-audit/weekly.sh >> /var/log/rdapq-harness-audit.log 2>&1
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
11
|
+
cd "$ROOT"
|
|
12
|
+
mkdir -p reports/harness-audit
|
|
13
|
+
|
|
14
|
+
python3_bin="/usr/bin/python3"
|
|
15
|
+
if [[ ! -x "$python3_bin" ]]; then
|
|
16
|
+
echo "error: $python3_bin is required" >&2
|
|
17
|
+
exit 2
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
"$python3_bin" scripts/harness-audit/check.py \
|
|
21
|
+
--report reports/harness-audit/latest.md \
|
|
22
|
+
--prompt reports/harness-audit/review-prompt.md
|
package/skills.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"skills": [
|
|
4
4
|
{
|
|
5
5
|
"name": "rdap-q",
|
|
6
|
-
"version": "1.2.
|
|
6
|
+
"version": "1.2.4",
|
|
7
7
|
"title": "RDAP-Q: Research-Driven Adaptive Planning with Quality Gates",
|
|
8
8
|
"description": "Vendor-neutral engineering protocol for evidence-first software development, persistent state, calibrated 0-10 scoring, and diminishing-return exit gates.",
|
|
9
9
|
"entrypoint": "rdap-q-skill/SKILL.md",
|