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.
Files changed (42) hide show
  1. package/.agents/plugins/rdap-q/plugin.json +1 -1
  2. package/.agents/skills/rdap-q/README.md +1 -1
  3. package/.agents/skills/rdap-q/SKILL.md +1 -1
  4. package/.agents/skills/rdap-q/install/install-unix.sh +1 -1
  5. package/.agents/skills/rdap-q/manifest.json +1 -1
  6. package/.claude/commands/rdapq.md +1 -1
  7. package/.claude-plugin/marketplace.json +1 -1
  8. package/.claude-plugin/plugin-index.json +1 -1
  9. package/.claude-plugin/plugin.json +1 -1
  10. package/.clinerules +1 -1
  11. package/.codex/skill.json +1 -1
  12. package/.github/copilot-instructions.md +1 -1
  13. package/.goosehints +1 -1
  14. package/.grok/rules.md +1 -1
  15. package/.grok/skill.json +1 -1
  16. package/.grok-plugin/marketplace.json +1 -1
  17. package/.grok-plugin/plugin-index.json +1 -1
  18. package/AGENTS.md +1 -1
  19. package/CLAUDE.md +1 -1
  20. package/GEMINI.md +1 -1
  21. package/README.md +90 -224
  22. package/agentskills.json +1 -1
  23. package/install.ps1 +2 -2
  24. package/install.sh +2 -2
  25. package/lib/installer.js +43 -27
  26. package/manifest.json +1 -1
  27. package/marketplace.json +1 -1
  28. package/package.json +3 -2
  29. package/plugin.json +1 -1
  30. package/plugins/rdap-q/plugin.json +1 -1
  31. package/plugins/rdap-q/skills/rdap-q/README.md +1 -1
  32. package/plugins/rdap-q/skills/rdap-q/SKILL.md +1 -1
  33. package/plugins/rdap-q/skills/rdap-q/install/install-unix.sh +1 -1
  34. package/plugins/rdap-q/skills/rdap-q/manifest.json +1 -1
  35. package/rdap-q-skill/README.md +1 -1
  36. package/rdap-q-skill/SKILL.md +1 -1
  37. package/rdap-q-skill/install/install-unix.sh +1 -1
  38. package/rdap-q-skill/manifest.json +1 -1
  39. package/scripts/harness-audit/check.py +387 -0
  40. package/scripts/harness-audit/vendors.json +192 -0
  41. package/scripts/harness-audit/weekly.sh +22 -0
  42. 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.1",
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",