@caddis/cli 0.2.1 → 0.2.3

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "caddis",
3
- "version": "1.3.40",
3
+ "version": "1.3.47",
4
4
  "description": "caddis — agent-agnostic dev harness as an Antigravity (agy) plugin: TDD skills + commands, single-sourced with the Claude Code plugin."
5
5
  }
@@ -16,6 +16,7 @@
16
16
  | Code Review | `code-review/` | Automated code review for pull requests using specialized review patterns. Analyzes code for quality, security, performance, and best practices. Use when reviewing code changes, PRs, or doing code audits. |
17
17
  | Codebase Audit | `codebase-audit/` | Systematic codebase audit methodology for unfamiliar codebases before architecture or implementation work. Use for new codebase, unfamiliar codebase, pre-implementation audit, codebase review, technical due diligence, or onboarding to an existing project. Produces AUDIT-FINDINGS.md and QUESTIONS.md. Routes to Architect, Planner, or Code Reviewer agents. |
18
18
  | Cross Review | `cross-review/` | Cross-vendor code review of the current diff — a second-vendor model (DeepSeek/GLM/any OpenAI-compatible endpoint) reviews your changes to catch bugs a same-vendor reviewer misses |
19
+ | Dashboard Perf Sweep | `dashboard-perf-sweep/` | Re-measure an app's dashboard/API endpoints for load-time regressions and decide, with a documented checklist (not a black-box verdict), whether a slow one is a candidate for the rs-kit summary-table refresher pattern — or needs an index, keyset paging, gzip, or nothing at all. Use when the user says "sweep dashboard performance", "check if this app needs rs-kit", "re-measure the perf tracker", "is this endpoint a refresher candidate", or when a dashboard-performance-tracker.md is stale and needs re-validating. Read-only by default (dev servers only; prod requires an explicit opt-in and human confirmation) and human-invoked, never scheduled -- it surfaces candidates, it does not enroll them. |
19
20
  | Error Handling | `error-handling/` | Error handling patterns for Python and TypeScript applications. Use when designing error hierarchies, implementing retry logic, building error boundaries, or establishing logging strategies. Covers custom exceptions, result types, circuit breakers, and user-facing error messages. |
20
21
  | Fastapi Dev | `fastapi-dev/` | Build FastAPI backends with standard patterns, error handling, and testing |
21
22
  | Javascript Typescript | `javascript-typescript/` | JavaScript and TypeScript development with ES6+, Node.js, React, and modern web frameworks. Use for frontend, backend, or full-stack JavaScript/TypeScript projects. |
@@ -46,6 +47,7 @@
46
47
  | Skill | Path | When to Use |
47
48
  |-------|------|-------------|
48
49
  | Code Documentation | `code-documentation/` | Writing effective code documentation - API docs, README files, inline comments, and technical guides. Use for documenting codebases, APIs, or writing developer guides. |
50
+ | Explainer Doc | `explainer-doc/` | Write a document that hands off work to a team who did NOT do the investigation — an external vendor, another team, a contractor — and who must be able to act on it alone. Use when the user says "write a spec for the X team", "document this for someone outside the team", "explainer doc", "implementation spec for a vendor", "hand this off to Y", or is about to write a design doc whose primary reader did not live through the debugging/design process. NOT for internal specs where the reader already has full context (use technical-writing or writing-plans instead) — this is specifically for the "stranger must follow it alone" case. |
49
51
  | Technical Writing | `technical-writing/` | Technical documentation best practices for READMEs, API docs, architecture docs, runbooks, and developer guides. Use when writing or reviewing documentation, creating onboarding guides, or establishing documentation standards. |
50
52
  | Writing Plans | `writing-plans/` | Use when you have a spec or requirements for a multi-step task, before touching code |
51
53
 
@@ -17,11 +17,14 @@ overridable via `CADDIS_KEYS_FILE`). If your keys file is populated, no env setu
17
17
  Details: the `coding/cross-review` skill and the **Providers & keys** guide.
18
18
 
19
19
  ## Run it
20
- The tool ships at `.github/tools/oss_review.py` (stdlib-only, no install):
20
+ Locate the tool (stdlib-only, no install), in order:
21
+ - `.github/tools/oss_review.py` — this project vendors its own copy.
22
+ - `${CLAUDE_PLUGIN_ROOT}/scripts/oss_review.py` — the plugin-bundled copy (every install has this;
23
+ use it whenever the project has no local copy).
21
24
  ```
22
- python .github/tools/oss_review.py # DeepSeek eyes (default)
23
- python .github/tools/oss_review.py --provider glm # GLM eyes
24
- python .github/tools/oss_review.py --range $ARGUMENTS # review a git range, e.g. origin/main..HEAD
25
+ python <resolved-path>/oss_review.py # DeepSeek eyes (default)
26
+ python <resolved-path>/oss_review.py --provider glm # GLM eyes
27
+ python <resolved-path>/oss_review.py --range $ARGUMENTS # review a git range, e.g. origin/main..HEAD
25
28
  ```
26
29
  Optional: `--base-url <url>`, `--model <id>` (env always overrides the preset).
27
30
  **Alternate the provider between reviews** (DeepSeek one diff, GLM the next) — different model
@@ -0,0 +1,132 @@
1
+ ---
2
+ name: dashboard-perf-sweep
3
+ context: fork
4
+ description: Re-measure an app's dashboard/API endpoints for load-time regressions and decide, with a documented checklist (not a black-box verdict), whether a slow one is a candidate for the rs-kit summary-table refresher pattern — or needs an index, keyset paging, gzip, or nothing at all. Use when the user says "sweep dashboard performance", "check if this app needs rs-kit", "re-measure the perf tracker", "is this endpoint a refresher candidate", or when a dashboard-performance-tracker.md is stale and needs re-validating. Read-only by default (dev servers only; prod requires an explicit opt-in and human confirmation) and human-invoked, never scheduled -- it surfaces candidates, it does not enroll them.
5
+ ---
6
+
7
+ # dashboard-perf-sweep — re-measure, then judge the shape, not just the time
8
+
9
+ Today an app "earns" the rs-kit refresher pattern only when someone happens to be doing unrelated
10
+ work on it, hits a slow page, and measures it properly. That worked for rev-sight and serve-sight,
11
+ but a snapshot measurement goes stale — a page added last month, or a table that crossed a growth
12
+ threshold since, has no mechanism to surface itself. This skill re-measures on demand.
13
+
14
+ **What already exists and must NOT be rebuilt:** every app in the fleet has its own
15
+ `.caddis/kb/dashboard-performance-tracker.md` with real baseline measurements. The gap this closes
16
+ is **re-measurement and re-judgment**, not first-time discovery.
17
+
18
+ ## Explicit scope guardrails — do not relax without a new conversation
19
+
20
+ - **Read-only except the final tracker update.** No code changes, no schema changes, ever. The only
21
+ write this skill makes is a diffable update to the target app's own `dashboard-performance-tracker.md`.
22
+ - **Dev by default.** Measuring against prod requires an explicit opt-in flag from the user AND, per
23
+ this project's convention for risky/hard-to-reverse actions, confirmation before hitting a live
24
+ service.
25
+ - **Human-invoked, not scheduled.** Run this when a person chooses to — never wire it into a cron job
26
+ or background automation. It surfaces candidates; it does not enroll them.
27
+ - **rs-kit is one of several possible recommendations, never the only output.** The checklist below
28
+ must be able to conclude "needs an index," "needs keyset paging," "needs gzip/trimmed fields,"
29
+ "not currently a candidate" — recommending rs-kit only for the specific aggregation shape.
30
+
31
+ ## The decision model — two gates, not one number
32
+
33
+ **Gate 1 — time (mechanical).** `DASHBOARD-PERFORMANCE-STANDARD.md` Rule 6: the first, uncached load
34
+ of any dashboard endpoint must finish in **under ~1 second**. The harness script below applies this
35
+ gate automatically per endpoint. Passing gate 1 does **not** mean rs-kit — it just means "look closer."
36
+
37
+ **Gate 2 — shape (judgment, applied by you the agent, never automated).** rs-kit fixes exactly one
38
+ shape: a query that recomputes `O(all rows ever stored)` per request, against data that keeps
39
+ growing. Time alone is not sufficient evidence — the fleet's own history proves it:
40
+
41
+ | App | Cold time | Root cause | rs-kit? |
42
+ |---|---|---|---|
43
+ | serve-sight | worst >60s, 5 real breaches | recurring full-table aggregation | **YES** |
44
+ | rev-sight (pre-fix) | 14s–60s+ across several endpoints | recurring full-table aggregation | **YES** |
45
+ | appointment-assist | 10.3s | a single ~158MB LOB payload on 9.1k rows, not aggregation | **NO** — SQL aggregate + gzip |
46
+ | nps-lens | 1.9s | cold JSON parse, no DB at all in prod | **NO** — warm the provider |
47
+ | app-sight | 202ms | n/a — never crossed gate 1 | **NO** |
48
+
49
+ A naive "slow ⇒ recommend rs-kit" tool would have wrongly flagged appointment-assist and nps-lens.
50
+ That already happened once before the shape distinction was made explicit — don't repeat it.
51
+
52
+ ## Step 1 — build the endpoint list for this app
53
+
54
+ The tracker files are semi-structured (page names + backtick route fragments in a markdown table),
55
+ not a clean machine-parseable list — extract the list yourself, per app, each run:
56
+
57
+ 1. Read the target app's `.caddis/kb/dashboard-performance-tracker.md` — pull every "Page / endpoint"
58
+ entry, in particular anything marked ⏳ pending or not re-measured recently.
59
+ 2. If the app has a `UI_PAGE_GUIDE.md` (check root and `docs/reference/`), cross-check it for pages
60
+ **not yet in the tracker** — this is what catches drift, not just re-running the same old list. Not
61
+ every app has one; if it's missing, say so explicitly rather than silently only using the tracker.
62
+ 3. Write the result as a JSON file matching the harness's input shape:
63
+ ```json
64
+ [
65
+ {"name": "Historical Trends chart", "method": "GET", "path": "/api/trends/daily"},
66
+ {"name": "IW grid", "method": "POST", "path": "/raworkbench/investigation/query", "json": {}}
67
+ ]
68
+ ```
69
+ 4. Confirm with the user (or the app's own `run`/`webapp-testing` conventions) how to reach the dev
70
+ instance safely, and what auth the endpoints need (open / token / session cookie) — pass it via
71
+ `--header`.
72
+
73
+ ## Step 2 — start the dev server, then run the harness
74
+
75
+ Use the app's own `run` skill/convention to start it — this skill does not start dev servers itself,
76
+ only measures an already-reachable URL.
77
+
78
+ **Use `127.0.0.1`, never the hostname `localhost`, in `--base-url`.** On Windows, Python's `urllib`
79
+ resolves `localhost` dual-stack (tries IPv6 `::1` first) and can add a **spurious, rock-steady
80
+ ~1000ms** to every single request — cold *and* warm, on every endpoint including the framework's own
81
+ static docs page. It looks exactly like a real, uniform perf problem (equal cold/warm, present on
82
+ trivial routes too) and is not one. This cost real time during this skill's own validation (§
83
+ Validating below) before `127.0.0.1` was substituted and the true numbers — sub-15ms — appeared. If a
84
+ sweep ever comes back with a suspiciously uniform floor across unrelated endpoints, first time a
85
+ `/docs` or `/openapi.json` route on the same base URL before trusting the data.
86
+
87
+ ```bash
88
+ python <skill-dir>/scripts/perf_sweep.py --base-url http://127.0.0.1:<port> --endpoints endpoints.json
89
+ python <skill-dir>/scripts/perf_sweep.py --base-url http://127.0.0.1:<port> --endpoints endpoints.json \
90
+ --header "Cookie: session=..." --output report.json
91
+ ```
92
+
93
+ Cold = first request after the dev server's fresh start (or right after a cache-bust if it can't be
94
+ restarted cheaply); warm = the immediate second request. The report flags gate-1 breaches and prints
95
+ the cold/warm ratio — a big ratio with no cache in between already points at a query-shape problem,
96
+ not a cache-TTL one. **A page that errors or times out is a finding, not a script failure** — it's
97
+ printed as `[ERROR]`, never silently dropped.
98
+
99
+ ## Step 3 — apply the gate-2 checklist to every gate-1 breach
100
+
101
+ Force these questions, in order, for each slow endpoint — don't skip to a verdict:
102
+
103
+ 1. **Does response size explain most of the time** (large payload, not a slow query)? → not rs-kit;
104
+ recommend trimming fields / gzip / virtualization.
105
+ 2. **Is the slow part parsing/serialization, not the DB round-trip?** → not rs-kit; recommend
106
+ caching/warming the parsed form (nps-lens precedent).
107
+ 3. **Does the query scale with total stored rows rather than the response size** — read the actual
108
+ SQL/query plan, not just the timing — **and is the same aggregation recomputed per request on data
109
+ that keeps growing?** → candidate for rs-kit.
110
+ 4. **Is it slow only at deep pagination (OFFSET)?** → not rs-kit; recommend keyset paging
111
+ (`exception_id`-style cursor, rev-sight R6/P3 precedent).
112
+ 5. **Is a specific WHERE clause unseekable** (function-wrapped column, NVARCHAR/VARCHAR param
113
+ mismatch)? → not rs-kit; recommend an index or a param-type fix.
114
+
115
+ Link findings to real fleet precedent (the table above, the `nvarchar-param-varchar-column-scan`
116
+ pattern, the keyset-paging work) rather than describing fixes in the abstract — every category here
117
+ has already been solved once in this fleet.
118
+
119
+ ## Step 4 — update the tracker, diffably
120
+
121
+ Update the target app's own `dashboard-performance-tracker.md` in place with the new measurements and
122
+ verdicts, so drift becomes visible over time in the diff. Never silently overwrite it without the user
123
+ being able to see a before/after. Print a summary alongside the file edit.
124
+
125
+ ## Validating this skill against known ground truth
126
+
127
+ Before trusting a new app's output, or after changing the checklist, run the sweep against an
128
+ already-known app and confirm it reproduces the **already-established, human-verified** conclusion:
129
+ **app-sight** (202ms, should conclude "not a candidate") and **serve-sight** (known breach, should
130
+ conclude "aggregation-shaped, rs-kit candidate" for the pages already identified). If the output
131
+ disagrees with the known-good conclusion for either, that's a checklist bug — fix the checklist, don't
132
+ rationalize the disagreement away.
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env python3
2
+ """Cold/warm HTTP timing harness for the dashboard-perf-sweep skill.
3
+
4
+ Given a base URL and a list of endpoints, measures a COLD request (first hit) and a WARM
5
+ request (immediately after) per endpoint, safely: a timeout or connection error on one endpoint
6
+ is recorded as a finding, never a crash of the whole sweep (DASHBOARD-PERFORMANCE-STANDARD.md
7
+ Rule 6 gate-1 mechanics; the huge cold/warm gap without a cache in between is itself diagnostic
8
+ of a query-shape problem, not a cache-TTL one).
9
+
10
+ This harness does NOT start dev servers or know anything about a specific app -- that's the
11
+ `run` skill's job. It only measures a URL that is already reachable. stdlib-only (urllib), no
12
+ install, matching the other caddis tool scripts (oss_review.py etc.).
13
+
14
+ CLI:
15
+ python perf_sweep.py --base-url http://localhost:8000 --endpoints endpoints.json
16
+ python perf_sweep.py --base-url http://localhost:8000 --endpoints endpoints.json --output report.json
17
+ python perf_sweep.py --base-url http://localhost:8000 --endpoints endpoints.json --gate1-seconds 1.0
18
+
19
+ endpoints.json shape (a JSON list):
20
+ [
21
+ {"name": "Historical Trends chart", "method": "GET", "path": "/api/trends/daily"},
22
+ {"name": "IW grid", "method": "POST", "path": "/raworkbench/investigation/query",
23
+ "json": {"filters": {}}, "headers": {"Authorization": "Bearer ..."}}
24
+ ]
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import json
30
+ import sys
31
+ import time
32
+ import urllib.error
33
+ import urllib.request
34
+ from dataclasses import asdict, dataclass, field
35
+ from typing import Any
36
+
37
+ DEFAULT_TIMEOUT_SECONDS = 30.0
38
+ DEFAULT_GATE1_SECONDS = 1.0 # DASHBOARD-PERFORMANCE-STANDARD.md Rule 6
39
+
40
+
41
+ @dataclass
42
+ class RequestResult:
43
+ ok: bool
44
+ status: int | None = None
45
+ elapsed_ms: float | None = None
46
+ error: str | None = None
47
+
48
+
49
+ @dataclass
50
+ class EndpointResult:
51
+ name: str
52
+ method: str
53
+ path: str
54
+ cold: RequestResult
55
+ warm: RequestResult | None = None
56
+ gate1_breach: bool = False
57
+
58
+
59
+ @dataclass
60
+ class SweepReport:
61
+ base_url: str
62
+ gate1_seconds: float
63
+ endpoints: list[EndpointResult] = field(default_factory=list)
64
+
65
+ def to_dict(self) -> dict[str, Any]:
66
+ return {
67
+ "base_url": self.base_url,
68
+ "gate1_seconds": self.gate1_seconds,
69
+ "endpoints": [
70
+ {
71
+ "name": e.name,
72
+ "method": e.method,
73
+ "path": e.path,
74
+ "cold": asdict(e.cold),
75
+ "warm": asdict(e.warm) if e.warm else None,
76
+ "gate1_breach": e.gate1_breach,
77
+ }
78
+ for e in self.endpoints
79
+ ],
80
+ }
81
+
82
+
83
+ def _do_request(
84
+ url: str,
85
+ method: str,
86
+ *,
87
+ json_body: dict[str, Any] | None = None,
88
+ headers: dict[str, str] | None = None,
89
+ timeout: float = DEFAULT_TIMEOUT_SECONDS,
90
+ ) -> RequestResult:
91
+ """One timed HTTP request. Never raises -- a timeout/connection/HTTP error becomes a
92
+ RequestResult with ok=False and a human-readable error, which is itself a sweep finding."""
93
+ data = None
94
+ req_headers = dict(headers or {})
95
+ if json_body is not None:
96
+ data = json.dumps(json_body).encode("utf-8")
97
+ req_headers.setdefault("Content-Type", "application/json")
98
+
99
+ req = urllib.request.Request(url, data=data, headers=req_headers, method=method)
100
+ start = time.perf_counter()
101
+ try:
102
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
103
+ resp.read()
104
+ elapsed_ms = (time.perf_counter() - start) * 1000.0
105
+ return RequestResult(ok=True, status=resp.status, elapsed_ms=elapsed_ms)
106
+ except urllib.error.HTTPError as exc:
107
+ # An HTTP error status still completed a round trip -- timing is a real finding.
108
+ elapsed_ms = (time.perf_counter() - start) * 1000.0
109
+ return RequestResult(ok=False, status=exc.code, elapsed_ms=elapsed_ms, error=f"HTTP {exc.code}")
110
+ except urllib.error.URLError as exc:
111
+ elapsed_ms = (time.perf_counter() - start) * 1000.0
112
+ return RequestResult(ok=False, elapsed_ms=elapsed_ms, error=f"connection error: {exc.reason}")
113
+ except TimeoutError:
114
+ elapsed_ms = (time.perf_counter() - start) * 1000.0
115
+ return RequestResult(ok=False, elapsed_ms=elapsed_ms, error=f"timeout after {timeout}s")
116
+
117
+
118
+ def sweep_endpoint(
119
+ base_url: str,
120
+ endpoint: dict[str, Any],
121
+ *,
122
+ timeout: float = DEFAULT_TIMEOUT_SECONDS,
123
+ gate1_seconds: float = DEFAULT_GATE1_SECONDS,
124
+ warm: bool = True,
125
+ extra_headers: dict[str, str] | None = None,
126
+ ) -> EndpointResult:
127
+ method = endpoint.get("method", "GET").upper()
128
+ path = endpoint["path"]
129
+ url = base_url.rstrip("/") + path
130
+ headers = dict(extra_headers or {})
131
+ headers.update(endpoint.get("headers", {}))
132
+ json_body = endpoint.get("json")
133
+
134
+ cold = _do_request(url, method, json_body=json_body, headers=headers, timeout=timeout)
135
+ warm_result = None
136
+ if warm:
137
+ warm_result = _do_request(url, method, json_body=json_body, headers=headers, timeout=timeout)
138
+
139
+ gate1_breach = bool(cold.ok and cold.elapsed_ms is not None and cold.elapsed_ms / 1000.0 > gate1_seconds)
140
+ return EndpointResult(
141
+ name=endpoint.get("name", path),
142
+ method=method,
143
+ path=path,
144
+ cold=cold,
145
+ warm=warm_result,
146
+ gate1_breach=gate1_breach,
147
+ )
148
+
149
+
150
+ def sweep(
151
+ base_url: str,
152
+ endpoints: list[dict[str, Any]],
153
+ *,
154
+ timeout: float = DEFAULT_TIMEOUT_SECONDS,
155
+ gate1_seconds: float = DEFAULT_GATE1_SECONDS,
156
+ warm: bool = True,
157
+ extra_headers: dict[str, str] | None = None,
158
+ ) -> SweepReport:
159
+ report = SweepReport(base_url=base_url, gate1_seconds=gate1_seconds)
160
+ for endpoint in endpoints:
161
+ report.endpoints.append(
162
+ sweep_endpoint(
163
+ base_url,
164
+ endpoint,
165
+ timeout=timeout,
166
+ gate1_seconds=gate1_seconds,
167
+ warm=warm,
168
+ extra_headers=extra_headers,
169
+ )
170
+ )
171
+ return report
172
+
173
+
174
+ def format_report(report: SweepReport) -> str:
175
+ lines = [f"Sweep: {report.base_url} (gate 1 = {report.gate1_seconds}s cold, uncached)", ""]
176
+ for e in report.endpoints:
177
+ if not e.cold.ok:
178
+ lines.append(f" [ERROR] {e.method:5s} {e.path:45s} {e.cold.error}")
179
+ continue
180
+ flag = "SLOW " if e.gate1_breach else " "
181
+ cold_s = f"{e.cold.elapsed_ms:8.1f}ms"
182
+ warm_s = f"{e.warm.elapsed_ms:8.1f}ms" if e.warm and e.warm.ok else "n/a"
183
+ gap = ""
184
+ if e.warm and e.warm.ok and e.cold.elapsed_ms and e.warm.elapsed_ms:
185
+ ratio = e.cold.elapsed_ms / max(e.warm.elapsed_ms, 0.01)
186
+ if ratio > 3:
187
+ gap = f" (cold/warm {ratio:.1f}x -- likely query-shape, not cache-TTL)"
188
+ lines.append(f" {flag}{e.method:5s} {e.path:45s} cold={cold_s} warm={warm_s}{gap}")
189
+ breaches = [e for e in report.endpoints if e.gate1_breach]
190
+ errors = [e for e in report.endpoints if not e.cold.ok]
191
+ problems = breaches + errors
192
+ lines.append("")
193
+ lines.append(
194
+ f"{len(problems)}/{len(report.endpoints)} endpoint(s) have a problem "
195
+ f"({len(breaches)} slow, {len(errors)} errored/timed out -- a timeout is a WORSE "
196
+ f"signal than a gate-1 breach, not a lesser one)."
197
+ )
198
+ if problems:
199
+ lines.append("Not itself a rs-kit verdict -- apply the shape checklist (SKILL.md) next.")
200
+ return "\n".join(lines)
201
+
202
+
203
+ def _parse_header(value: str) -> tuple[str, str]:
204
+ if ":" not in value:
205
+ raise argparse.ArgumentTypeError(f"--header must be 'Key: Value', got {value!r}")
206
+ key, _, val = value.partition(":")
207
+ return key.strip(), val.strip()
208
+
209
+
210
+ def main(argv: list[str] | None = None) -> int:
211
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
212
+ parser.add_argument("--base-url", required=True, help="e.g. http://localhost:8703")
213
+ parser.add_argument("--endpoints", required=True, help="path to a JSON file (see module docstring)")
214
+ parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT_SECONDS)
215
+ parser.add_argument("--gate1-seconds", type=float, default=DEFAULT_GATE1_SECONDS)
216
+ parser.add_argument("--no-warm", action="store_true", help="skip the warm (second) request")
217
+ parser.add_argument("--header", action="append", default=[], type=_parse_header,
218
+ help="extra header applied to every request, repeatable: --header 'Cookie: session=...'")
219
+ parser.add_argument("--output", help="also write the JSON report to this path")
220
+ args = parser.parse_args(argv)
221
+
222
+ with open(args.endpoints, encoding="utf-8") as f:
223
+ endpoints = json.load(f)
224
+ if not isinstance(endpoints, list):
225
+ print(f"[FAIL] {args.endpoints} must contain a JSON list of endpoint objects", file=sys.stderr)
226
+ return 2
227
+
228
+ report = sweep(
229
+ args.base_url,
230
+ endpoints,
231
+ timeout=args.timeout,
232
+ gate1_seconds=args.gate1_seconds,
233
+ warm=not args.no_warm,
234
+ extra_headers=dict(args.header),
235
+ )
236
+
237
+ print(format_report(report))
238
+ if args.output:
239
+ with open(args.output, "w", encoding="utf-8") as f:
240
+ json.dump(report.to_dict(), f, indent=2)
241
+ print(f"\nWrote {args.output}")
242
+
243
+ breaches = [e for e in report.endpoints if e.gate1_breach]
244
+ errors = [e for e in report.endpoints if not e.cold.ok]
245
+ return 1 if (breaches or errors) else 0
246
+
247
+
248
+ if __name__ == "__main__":
249
+ raise SystemExit(main())
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: explainer-doc
3
+ context: fork
4
+ description: Write a document that hands off work to a team who did NOT do the investigation — an external vendor, another team, a contractor — and who must be able to act on it alone. Use when the user says "write a spec for the X team", "document this for someone outside the team", "explainer doc", "implementation spec for a vendor", "hand this off to Y", or is about to write a design doc whose primary reader did not live through the debugging/design process. NOT for internal specs where the reader already has full context (use technical-writing or writing-plans instead) — this is specifically for the "stranger must follow it alone" case.
5
+ ---
6
+
7
+ # explainer-doc — write for the reader who has no other context
8
+
9
+ A spec written by the people who did the investigation is unreadable by the team who must implement
10
+ it. The authors compress away the reasoning, so every design choice looks arbitrary, and the reader
11
+ either follows it blindly or argues with it. **The fix is not "add more detail" — it is to lead with
12
+ *why*, and to make the document survive being read alone.**
13
+
14
+ Proven in anger on a Genesys→ETL cutover spec handed to an external vendor team. The user's verdict:
15
+ *"nailed it — the document was perfect, simple, this should be the standard for explainer documents."*
16
+ Reference implementation to study: `serve-sight/docs/genesys-cutover/etl-implementation-spec.md` (full
17
+ spec) and `pega-cticallid-change-request.md` (the same pattern compressed to one page for a single ask).
18
+
19
+ ## When this is NOT the right skill
20
+
21
+ If the reader already lived through the investigation (a teammate, your future self, an internal
22
+ handoff with shared context), use `technical-writing` or `writing-plans` instead — those are lighter
23
+ and don't need the "assume zero shared context" discipline below. Reach for `explainer-doc` specifically
24
+ when the reader is a **stranger to the problem**: an external vendor, a different team, a contractor,
25
+ anyone who will act on the document with no one in the room to ask.
26
+
27
+ ## The shape — in order. The order IS the pattern
28
+
29
+ Do not reorder these. Each section's position is load-bearing: later sections lean on earlier ones for
30
+ their justification, and a reader working top-to-bottom needs the "why" before the "what."
31
+
32
+ | § | Section | Why it must come here |
33
+ |---|---|---|
34
+ | 1 | **Why this exists** — what broke, in a diagram | Every later decision is justified by this. Lead with the failure, not the solution. |
35
+ | 2 | **What we're asking for, in one picture** + a size table | The reader decides here whether the ask is reasonable. Include what you are explicitly **not** asking for. |
36
+ | 3 | **Understanding the source data** — the 3-5 things that catch people out | Each one should have already caused a real bug. Not a data dictionary. |
37
+ | 4 | **The one thing that matters most** | Usually not the new build. Say so plainly, with the number that proves it. |
38
+ | 5 | **Where it runs** + access needed | The blocker nobody writes down until the first deploy fails. |
39
+ | 6 | **What to build**, with the reasoning *beside* each choice | Not in an appendix. Next to the DDL / the query / the config. |
40
+ | 6b | **Changes to what already exists** | Always under-specified. Ask "what did I only half-mention?" — this is where the gaps hide. |
41
+ | 7-8 | The actual implementation (extract/build queries, load procedure, etc.) | |
42
+ | 9 | Schedule / retention / first load | The one-off backfill is always forgotten. |
43
+ | 10 | **Validation, with EXPECTED VALUES** | "Run this, expect ~64%" beats "verify the data." |
44
+ | 11 | Priority · acceptance (who signs off) · **what we excluded** | The exclusion list stops speculative building. |
45
+ | 12 | **Glossary** | Every acronym, inline. Not a link. |
46
+
47
+ Adapt section names to the domain (this table is ETL-flavored from its origin; a UI handoff, an API
48
+ contract, or an infra runbook will rename §6-9) — but keep the **order and the intent of each slot**.
49
+
50
+ ## The ten rules
51
+
52
+ 1. **Self-contained.** If the reader needs another document to follow this one, the document has
53
+ failed. Companions are optional background, stated as such — never load-bearing.
54
+ 2. **Lead with the failure mode, not the solution.** State what actually went wrong, concretely, before
55
+ proposing anything. Everything cautious later inherits its justification from that paragraph.
56
+ 3. **Reasoning beside the artefact, not in an appendix.** "Cluster on the date not the GUID" with the
57
+ 3-row comparison table right there. An appendix is never read.
58
+ 4. **Every number carries its measurement date and a way to reproduce it.** "1.4% of reality (measured
59
+ 2026-07-14, `SELECT ...`)" — not "a small fraction."
60
+ 5. **Say what you are NOT asking for.** Prevents speculative work and pre-answers "why not X?" before
61
+ it's asked.
62
+ 6. **Name what is not the reader's fault.** If a gap traces to a third system or a decision made
63
+ elsewhere, say so explicitly. Saves the reader a week of misdirected chasing and buys credibility for
64
+ the rest of the document.
65
+ 7. **Diagrams for the shape, tables for the detail, prose only for judgement.** No diagram should repeat
66
+ what a table already says better.
67
+ 8. **Validation states expected values.** "It ran" and "it is right" must be two different checks — give
68
+ the reader the number to compare against.
69
+ 9. **Route by owner.** If an item belongs to a third team, mark it as theirs, explicitly, in the document
70
+ — don't let it read as part of the reader's scope.
71
+ 10. **Glossary inline.** The reader will not follow a link mid-implementation; every acronym gets defined
72
+ where it's used, plus a glossary section as the fallback lookup.
73
+
74
+ ## Process
75
+
76
+ 1. **Confirm the reader.** Ask (or infer from context) who this is actually for and how much they
77
+ already know. If they have real shared context, stop and suggest `technical-writing`/`writing-plans`
78
+ instead — this skill's discipline is overhead they don't need.
79
+ 2. **Draft §1 first, alone, and get it right before writing anything else.** Every other section's
80
+ justification traces back to it. If you can't state the failure mode plainly and concretely, the
81
+ rest of the document will inherit that vagueness.
82
+ 3. **Write the shape table above as your outline**, adapting section names to the domain, then fill it
83
+ in top to bottom. Keep reasoning next to the artefact it justifies (rule 3), not in a separate
84
+ "design decisions" section.
85
+ 4. **Before sending, ask the two questions below** — they are an exit gate, not a suggestion.
86
+
87
+ ## The two-question exit gate
88
+
89
+ Ask both, honestly, before the document ships:
90
+
91
+ - **"Could someone who has read nothing else follow this end to end?"** If the answer requires "well,
92
+ they'd also need to know X" — X is either missing or should be a stated, explicit companion doc.
93
+ - **"What did I mention once and never specify?"** This is the question that surfaces the gaps — in the
94
+ origin case it found an entire missing section (§6b, changes to existing infrastructure) that had been
95
+ gestured at once and never actually written.
96
+
97
+ If either question fails, fix the document — don't ship it and hope the reader asks.
@@ -92,6 +92,15 @@ A tier can be served by an OSS provider instead of Anthropic (cheat sheet:
92
92
  (Claude or GLM implemented) or `--provider glm` / a Claude `code-reviewer` pass (DeepSeek-adjacent
93
93
  work). Never same-vendor.
94
94
 
95
+ ### Flag high-risk phases (`risk:` — a marker, not a mechanism)
96
+ Each phase may carry **`risk: normal|high`** (default `normal` — omit the line unless the phase is
97
+ `high`), meaning only "this is a phase where a wrong answer is expensive" — security/authz, a
98
+ correctness-critical algorithm, an ambiguous spec, or an irreversible change. It is **inert**: it
99
+ selects no model, spawns no agents, and implies no fan-out — it exists so the executor can decide **at
100
+ runtime** whether an extra cross-check is worth it. Do not emit orchestration, lane fan-out, or
101
+ `Workflow` scripts from this command; the runtime decision rules and the intent→model map live in one
102
+ place, the `agent-orchestration` skill's *Multi-model fan-out* section.
103
+
95
104
  ## Step 3 — Write the plan to `.caddis/plans/<feature-slug>.md`
96
105
 
97
106
  Create `.caddis/plans/` if it doesn't exist.
@@ -127,6 +136,7 @@ Creating Model: <model-id>
127
136
  **Model:** <tier> (`/model <alias>`) — <one-line rationale tied to this phase's difficulty>
128
137
  **Lane:** claude — this session | glm-headless — `claude-glm -p "/caddis:implement <this plan's path> — Phase 1 only"`
129
138
  **Session:** continue | **fresh** (start a new session for this phase — note why: lane change / heavy context)
139
+ **Risk:** high — token-verification seam; a wrong answer here is a silent auth bypass *(omit this line when normal)*
130
140
  **Goal:** <one sentence>
131
141
  **Touches:** `<files>`
132
142
  **TDD:**
@@ -46,8 +46,9 @@ surface the `[DOC-MAP]` "read the index first" pointer for future sessions in th
46
46
 
47
47
  ## KB note format (OKF-lite — mandatory for every new note)
48
48
 
49
- Every new `.caddis/kb/*.md` note starts with this frontmatter block (`type` is required;
50
- the rest recommended). `DOC-MAP.md` is the index, not a note — it stays frontmatter-free:
49
+ Every new `.caddis/kb/*.md` note starts with this frontmatter block. **`type` is the only required
50
+ field**; everything else is recommended. `DOC-MAP.md` is the index, not a note — it stays
51
+ frontmatter-free:
51
52
 
52
53
  ```yaml
53
54
  ---
@@ -56,5 +57,18 @@ title: <human title>
56
57
  description: <one line — keep it identical to the note's DOC-MAP row description>
57
58
  tags: [topic, topic]
58
59
  timestamp: 2026-01-01 # ISO date of last substantive update
60
+ # --- OKF v0.2 trust signals (all optional; omit rather than guess) ---
61
+ status: stable # draft | stable | deprecated (absent reads as stable)
62
+ stale_after: 2026-12-31 # re-verify by this date; nothing auto-acts on it
63
+ verified: # who CONFIRMED it (not who wrote it); append, never rewrite
64
+ - { by: human:handle, at: 2026-07-28T10:00:00Z }
65
+ generated: { by: caddis/<model-id>, at: 2026-07-28T09:00:00Z }
59
66
  ---
60
67
  ```
68
+
69
+ The full schema, the caddis↔OKF `status` mapping, and what caddis deliberately skips from OKF v0.2
70
+ (`sources:`, `Attested Computation`) live in
71
+ [`.github/instructions/document-frontmatter.instructions.md`](../../.github/instructions/document-frontmatter.instructions.md).
72
+
73
+ **Backward compatible:** an existing note with no trust fields is still correct. Do not bulk-migrate
74
+ old notes — add the fields as notes are next touched.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "caddis-extras",
3
- "version": "1.3.13",
3
+ "version": "1.3.20",
4
4
  "description": "caddis-extras — the long-tail skills for the Antigravity (agy) caddis plugin; install alongside caddis for the full set."
5
5
  }
@@ -136,7 +136,7 @@
136
136
  | Skill | Path | When to Use |
137
137
  |-------|------|-------------|
138
138
  | Agent Md Refactor | `agent-md-refactor/` | Refactor bloated agent instruction files (AGENTS.md, .cursorrules, .github/ files, etc.) to follow progressive disclosure principles. Splits monolithic files into organized, linked documentation. |
139
- | Agent Orchestration | `agent-orchestration/` | End-to-end blueprint for orchestrating the full agent pipeline — from spec intake through planning, implementation, testing, and debugging |
139
+ | Agent Orchestration | `agent-orchestration/` | End-to-end blueprint for orchestrating the full agent pipeline — from spec intake through planning, implementation, testing, and debugging — plus the multi-model fan-out rules (when a second model is worth it, the intent→model map, and the cost guardrails) |
140
140
  | Ask Questions If Underspecified | `asking-questions/` | Clarify requirements before implementing. Do not use automatically, only when invoked explicitly. |
141
141
  | Data Contract Pipeline | `data-contract-pipeline/` | **WORKFLOW SKILL** - Build, audit, and validate data-to-UI contracts for apps. Use whenever the user mentions data mapping, UI lineage, DB-to-UI, DisplayDTOs, source-to-screen mapping, data contracts, schema drift, typed API responses, frontend type alignment, mockup grounding, requirements-to-UI mapping, or asks whether a UI is backed by real DB/file data. Works for DBs, JSON, Markdown, CSV, XLSX, YAML, APIs, and UI mockups. |
142
142
  | Developer Growth Analysis | `developer-growth-analysis/` | Analyzes your recent Claude Code chat history to identify coding patterns, development gaps, and areas for improvement, curates relevant learning resources from HackerNews, and automatically sends a personalized growth report to your Slack DMs. |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: agent-orchestration
3
- description: "End-to-end blueprint for orchestrating the full agent pipeline — from spec intake through planning, implementation, testing, and debugging"
3
+ description: "End-to-end blueprint for orchestrating the full agent pipeline — from spec intake through planning, implementation, testing, and debugging — plus the multi-model fan-out rules (when a second model is worth it, the intent→model map, and the cost guardrails)"
4
4
  ---
5
5
 
6
6
  # Agent Orchestration Blueprint
@@ -52,6 +52,11 @@ The operator (human) acts as **orchestrator**. Agents act as an **advisory board
52
52
 
53
53
  Every stage produces a **persistent artefact** (markdown file, commit, test run) that the next stage can consume without relying on conversation memory. This is what makes multi-session execution reliable.
54
54
 
55
+ > The pipeline below is the **sequential** axis — which agent, in what order. There is a second,
56
+ > independent axis: **how many models** work a given stage. That is deliberately rationed — see
57
+ > [Multi-model fan-out](#multi-model-fan-out-the-second-axis-how-many-models) before reaching for a
58
+ > second model.
59
+
55
60
  | Stage | Artefact | Location |
56
61
  |-------|----------|----------|
57
62
  | Requirements | Spec / feature file | Supplied by operator (e.g. `nextprompt.md`) |
@@ -392,10 +397,138 @@ The plan document should contain **copy-paste prompts** for each phase. This rem
392
397
 
393
398
  ---
394
399
 
400
+ ## Multi-model fan-out (the second axis: how many models)
401
+
402
+ > ### ⚠️ If you are not Claude Code, treat this section as ADVISORY and execute solo.
403
+ > The `Agent` tool, its per-agent `model:` override, the `Workflow` tool, and the `/glm`,
404
+ > `/deepseek`, `/caddis:cross-review` commands described below are **Claude-Code-only**. On Codex,
405
+ > Copilot, Antigravity/agy, or any other host: take the *judgement* from this section — default solo,
406
+ > cheapest deterministic check first, don't trust a single model on a security invariant — and
407
+ > **ignore the mechanics**. Do not attempt to emulate fan-out by hand, do not tell the user to run
408
+ > tools your host does not have, and do not treat the model map below as instructions you can execute.
409
+ > Solo execution is a correct, complete outcome here.
410
+
411
+ Everything above is *sequential*: one model, one stage at a time, operator in the loop. **Fan-out** —
412
+ running two or more models on the same work and reconciling their output — is a separate axis, and it
413
+ is **expensive in three currencies at once**: provider quota, the orchestrator's own context, and the
414
+ human's attention. It earns its cost rarely. The rules below are ordered; apply them in order.
415
+
416
+ ### Rule 0 — default SOLO
417
+
418
+ **One model does the work. That is the default and it is right most of the time.** Fan out only when
419
+ the work is genuinely high-risk *or* high-uncertainty:
420
+
421
+ | Fan out | Stay solo |
422
+ |---|---|
423
+ | Security-sensitive code — authn/authz, crypto, secret handling, input trust boundaries | Boilerplate, CRUD, scaffolding |
424
+ | Correctness-critical algorithms where a subtle wrong answer looks right (concurrency, money, dates/timezones, migrations, dedup/merge logic) | Mechanical refactors with a green suite either side |
425
+ | Ambiguous specs — where the risk is that *everyone* misreads the requirement the same way | Anything a test, type-checker, or linter can decide |
426
+ | Verifying a frontier model's **own** risky output — a model is the worst reviewer of its own reasoning | Work already covered by an existing review gate |
427
+ | Irreversible / wide blast radius — a drop, a flag day, a fleet-wide sweep | Work you can trivially revert |
428
+
429
+ Two failure modes, both real: **fanning out on size** (a big mechanical task is still solo — split it
430
+ across *sessions*, not models) and **fanning out for reassurance** (a second opinion you have already
431
+ decided to ignore is pure cost).
432
+
433
+ ### Rule 1 — cheapest check first
434
+
435
+ **Never spend a second model on something a deterministic check can decide.** Climb this ladder and
436
+ stop at the first rung that answers the question:
437
+
438
+ 1. **Tests / types / linters / the build.** If a failing test, `mypy`/`tsc`, or a lint rule confirms
439
+ the concern, you are done — that is a *fact*, not an opinion, and it costs nothing.
440
+ 2. **Read the code.** A targeted grep or a five-line read beats any model's guess about what the code
441
+ does.
442
+ 3. **A subagent on the same model** (`code-reviewer`, `security-analyst`, `anchor`, `preflight`) — a
443
+ fresh context, not a fresh vendor. Catches most "did we miss something" cases.
444
+ 4. **A different tier** — escalate one model tier for the hard part only.
445
+ 5. **Cross-vendor.** The last rung, and the rationed one (see the guardrails).
446
+
447
+ If a deterministic check *can* be written but doesn't exist yet, **write the check** — it is cheaper
448
+ than the model call and it keeps paying.
449
+
450
+ ### Rule 2 — name the INTENT, not the model
451
+
452
+ Talk about lanes by what they are *for*, so plans, prompts, and reviews survive model releases and the
453
+ LiteLLM cutover. **This table is the single place the intent→model mapping lives** — everything else
454
+ (plans, commands, skills) refers to the intent name and points here.
455
+
456
+ | Intent | What it's for | Current model | Claude Code mechanics | Cost |
457
+ |---|---|---|---|---|
458
+ | **deepest** | Subtle correctness, security invariants, novel architecture, judgment-heavy seams | `opus` | `/model opus`; `Agent(model: "opus")` | **High** — Anthropic plan quota |
459
+ | **broad** | Wide parallel coverage of a large but not-subtle surface; the workhorse | `sonnet` | `/model sonnet`; `Agent(model: "sonnet")` | Medium — the default; budget for several |
460
+ | **mechanical** | Fully-specced, repeat-of-a-known-pattern, no judgment required | `haiku` | `/model haiku`; `Agent(model: "haiku")` | Low |
461
+ | **cross-vendor-reviewer** | A *different vendor's* blind spots on a diff — **code review only** | `deepseek` (default), `glm` | `/caddis:cross-review`, `/glm`, `/deepseek` | **Scarce** — separate starter-tier subscription, NOT Anthropic quota. Ration it. |
462
+ | **synthesis / long-horizon** | Orchestrating many lanes, or work that can't self-verify across a large codebase | `fable` | `/model fable` | **Very high** — ~2× opus's rate-limit burn. RARE. |
463
+
464
+ **Update this table, not the callers**, when a model is added, retired, or re-tiered. A plan that says
465
+ "cross-vendor-reviewer" keeps working; a plan that says "deepseek-chat" rots.
466
+
467
+ ### Hard guardrails
468
+
469
+ These are constraints, not preferences. Each exists because breaking it produced a real cost.
470
+
471
+ 1. **DeepSeek and GLM are the CODE-REVIEW lane, and nothing else.** Map them to cross-vendor review of
472
+ a diff or a concrete file set. **Never** general audit, research, planning, architecture, or
473
+ open-ended subagent work — they are unreliable outside that lane, and an unreliable lane's output
474
+ still costs a full synthesis pass to disprove.
475
+ 2. **Ration cross-vendor to ~one consolidated pass per task.** One review of the whole change, not one
476
+ agent per area. Alternate vendors *between* tasks (DeepSeek this diff, GLM the next) — different
477
+ families have different blind spots and alternation maximises what the pair catches over time.
478
+ 3. **Degrade to solo when a lane's quota is exhausted.** A 402/429/quota error on GLM or DeepSeek is a
479
+ **downgrade, not a blocker**: log that the cross-vendor pass was skipped, continue with the
480
+ same-vendor reviewer, and say so in the report. Never stall the task waiting on a rationed lane,
481
+ and never silently drop the pass — an unrun check that reads as "reviewed" is worse than no check.
482
+ 4. **Name the synthesis cost before you add a lane.** N lanes do not cost N model calls — they cost N
483
+ calls **plus** reconciling N reports, which burns the orchestrator's frontier context (the scarcest
484
+ thing in the session) **and** the human's attention on the write-up. So:
485
+ - **Default cap: 3 lanes.** Above that, write the synthesis budget down first — who reconciles, on
486
+ what model, and what the human is expected to read.
487
+ - **Hard ceiling ~8**, and only for a one-off audit of a genuinely partitioned surface. The 8-lane
488
+ migration audit that motivated these rules produced a 272-line report that still needed a human
489
+ read and a main-thread re-verification of every HIGH finding. That tail is the real cost.
490
+ - If you cannot say what you would *do differently* based on lane N's output, lane N is not worth
491
+ running.
492
+ 5. **Lanes advise; they do not write.** Fan-out agents are read-only — they return findings, the main
493
+ thread applies them. This is the same rule as "agents never chain autonomously without review", and
494
+ it is what keeps parallel lanes from clobbering each other.
495
+ 6. **Verify high-severity findings in the main thread before acting.** A finding from a lane is a
496
+ *claim*. Re-read the cited file/line yourself. Subagent reports are confidently wrong often enough
497
+ that acting on an unverified HIGH is how fan-out turns into net-negative work.
498
+
499
+ ### Running it — existing mechanisms only
500
+
501
+ Invent no new machinery. Everything needed already ships:
502
+
503
+ | Need | Use |
504
+ |---|---|
505
+ | Second opinion, same vendor, fresh context | `Agent` tool → `code-reviewer` / `security-analyst` / `anchor` / `preflight` |
506
+ | A different tier for one lane only | The `Agent` tool's `model:` override (per-agent; does not change the session model) |
507
+ | 2–4 lanes in parallel | Several `Agent` calls **in one message** — they run concurrently. This is enough; you do not need anything heavier. |
508
+ | Deterministic multi-stage orchestration (fan out over a discovered list, loop until dry, adversarial verify stages) | The `Workflow` tool — **only when the human explicitly asks for it**; it can spawn dozens of agents |
509
+ | Cross-vendor review of a diff | `/caddis:cross-review` (or `python .github/tools/oss_review.py --provider deepseek\|glm --range <range>`) |
510
+ | Cross-vendor opinion that needs to read the repo | `/glm <prompt>` or `/deepseek <prompt>` |
511
+ | Cross-vendor one-shot, no repo context | `/ask-glm` / `/ask-deepseek` |
512
+ | Per-phase model + lane assignment in a plan | `/caddis:feature-plan` — every phase already carries **Model**, **Lane**, and a cross-review provider |
513
+
514
+ **Reviewer rule (standing):** the reviewer is always a different vendor than whoever wrote the code.
515
+ Claude or GLM implemented → DeepSeek reviews (or swap). Never same-vendor.
516
+
517
+ Full command/billing decision table: `docs/guide/multi-model-workflow.md`.
518
+
519
+ ---
520
+
395
521
  ## Anti-Patterns to Avoid
396
522
 
397
523
  | ❌ Anti-Pattern | ✅ Instead |
398
524
  |----------------|-----------|
525
+ | Fanning out because the task is *big* | Fan out for **risk**, not size — split a big mechanical task across sessions, not models |
526
+ | Spending a cross-vendor pass before the tests have run | Cheapest check first — deterministic gates decide it for free |
527
+ | One cross-vendor agent per area | One consolidated cross-vendor pass per task — the lane is rationed |
528
+ | Using DeepSeek/GLM for audit, research, or planning | Code-review lane only — they are unreliable outside it |
529
+ | Adding an Nth lane without budgeting the synthesis | Cap the lanes; reconciling N reports lands on you *and* the human |
530
+ | Acting on a subagent's HIGH finding as reported | Re-verify it in the main thread against the cited file/line first |
531
+ | Blocking the task when a rationed lane is out of quota | Degrade to solo, log the skipped pass, continue |
399
532
  | Jumping straight to code without a plan | Triage → ADR (if needed) → Plan → Implement |
400
533
  | Implementing all phases in one session | One phase per session — clean context, focused work |
401
534
  | Letting agents edit the plan document | Only the Planner agent (or operator) edits plans |
@@ -493,6 +626,7 @@ A well-orchestrated session should produce:
493
626
  | Session continuation context | `.github/skills/workflow/relay/SKILL.md` |
494
627
  | Verifying code before committing | `.github/skills/workflow/verification-loop/SKILL.md` |
495
628
  | Writing implementation plans | `.github/skills/docs/writing-plans/SKILL.md` |
629
+ | Running the cross-vendor review lane | `.github/skills/coding/cross-review/SKILL.md` |
496
630
  | Creating new reusable skills | `.github/skills/workflow/skill-creator/SKILL.md` |
497
631
  | Best practices reference | `.github/skills/workflow/best-practices/SKILL.md` |
498
632
 
@@ -28,10 +28,13 @@ churn — the preset table in `oss_review.py` is the one place to edit a rename,
28
28
  `REVIEW_BASE_URL` always win, so you can point at any new id without touching code.
29
29
 
30
30
  ## How to run
31
- From the repo root (the tool ships at `.github/tools/oss_review.py`):
31
+ From the repo root. Locate the tool, in order:
32
+ - `.github/tools/oss_review.py` — this project vendors its own copy.
33
+ - `${CLAUDE_PLUGIN_ROOT}/scripts/oss_review.py` — the plugin-bundled copy (every Claude Code
34
+ install has this; use it whenever the project has no local copy).
32
35
  ```
33
- python .github/tools/oss_review.py # review the working tree (staged+unstaged)
34
- python .github/tools/oss_review.py --range origin/main..HEAD # review a branch's commits
36
+ python <resolved-path>/oss_review.py # review the working tree (staged+unstaged)
37
+ python <resolved-path>/oss_review.py --range origin/main..HEAD # review a branch's commits
35
38
  ```
36
39
  Optional flags: `--cwd <repo>`, `--base-url <url>`, `--model <id>` (override the env).
37
40
 
@@ -1,7 +1,7 @@
1
1
  {
2
- "poolVersion": "1.3.40",
2
+ "poolVersion": "1.3.47",
3
3
  "bundles": {
4
- "antigravity-plugin": "1.3.40",
5
- "antigravity-plugin-extras": "1.3.13"
4
+ "antigravity-plugin": "1.3.47",
5
+ "antigravity-plugin-extras": "1.3.20"
6
6
  }
7
7
  }
package/dist/cli.js CHANGED
@@ -5167,7 +5167,7 @@ var claudeAdapter = {
5167
5167
  };
5168
5168
 
5169
5169
  // src/agents/agy.ts
5170
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
5170
+ import { readFileSync as readFileSync2, existsSync as existsSync2, mkdtempSync, cpSync, rmSync } from "fs";
5171
5171
  import os from "os";
5172
5172
  import path2 from "path";
5173
5173
  init_pkg();
@@ -5239,6 +5239,25 @@ function shortenHome(target) {
5239
5239
  const home = os.homedir();
5240
5240
  return target.startsWith(home) ? path2.join("~", target.slice(home.length)) : target;
5241
5241
  }
5242
+ function stageForAgy(bundleDir) {
5243
+ if (!bundleDir.includes("@")) {
5244
+ return { installPath: bundleDir, cleanup: () => {
5245
+ } };
5246
+ }
5247
+ const staged = mkdtempSync(path2.join(os.tmpdir(), "caddis-agy-"));
5248
+ const dest = path2.join(staged, path2.basename(bundleDir));
5249
+ cpSync(bundleDir, dest, { recursive: true });
5250
+ return {
5251
+ installPath: dest,
5252
+ // Best-effort: a stray temp dir left behind is harmless clutter, not a failure.
5253
+ cleanup: () => {
5254
+ try {
5255
+ rmSync(staged, { recursive: true, force: true });
5256
+ } catch {
5257
+ }
5258
+ }
5259
+ };
5260
+ }
5242
5261
  async function drive2(_action, options) {
5243
5262
  const detected = await detect2();
5244
5263
  if (!detected.present) {
@@ -5253,12 +5272,12 @@ async function drive2(_action, options) {
5253
5272
  message: `the ${BUNDLE} bundle is missing from this package \u2014 reinstall @caddis/cli`
5254
5273
  };
5255
5274
  }
5256
- const planned = [{ cmd: BIN2, args: ["plugin", "install", bundle] }];
5275
+ const planned = [{ cmd: BIN2, args: ["plugin", "install", bundle], bundleDir: bundle }];
5257
5276
  const extrasInstalled = extrasStatusFromHome(os.homedir()).installed;
5258
5277
  if (options.extras === true || extrasInstalled) {
5259
5278
  const extrasBundle = bundlePath(EXTRAS_BUNDLE);
5260
5279
  if (extrasBundle) {
5261
- planned.push({ cmd: BIN2, args: ["plugin", "install", extrasBundle] });
5280
+ planned.push({ cmd: BIN2, args: ["plugin", "install", extrasBundle], bundleDir: extrasBundle });
5262
5281
  } else if (options.extras === true) {
5263
5282
  return {
5264
5283
  ok: false,
@@ -5272,25 +5291,34 @@ async function drive2(_action, options) {
5272
5291
  return {
5273
5292
  ok: true,
5274
5293
  skipped: true,
5294
+ // Show the logical bundle path, not a staged temp copy that was never created.
5275
5295
  steps: planned.map((s) => ({ command: formatCommand(s.cmd, s.args), ok: true, code: null })),
5276
5296
  message: "dry run \u2014 nothing executed"
5277
5297
  };
5278
5298
  }
5279
- const steps2 = [];
5280
- for (const step of planned) {
5281
- const command = formatCommand(step.cmd, step.args);
5282
- const result = await run(step.cmd, step.args);
5283
- steps2.push({
5284
- command,
5285
- ok: result.ok,
5286
- code: result.code,
5287
- output: result.ok ? void 0 : (result.stderr || result.stdout || result.failure || "").trim().split(/\r?\n/).slice(-3).join("\n")
5288
- });
5289
- if (!result.ok) {
5290
- return { ok: false, skipped: false, steps: steps2, message: `\`${command}\` failed` };
5299
+ const cleanups = [];
5300
+ try {
5301
+ const steps2 = [];
5302
+ for (const step of planned) {
5303
+ const command = formatCommand(step.cmd, step.args);
5304
+ const staged = stageForAgy(step.bundleDir);
5305
+ cleanups.push(staged.cleanup);
5306
+ const execArgs = [...step.args.slice(0, -1), staged.installPath];
5307
+ const result = await run(step.cmd, execArgs);
5308
+ steps2.push({
5309
+ command,
5310
+ ok: result.ok,
5311
+ code: result.code,
5312
+ output: result.ok ? void 0 : (result.stderr || result.stdout || result.failure || "").trim().split(/\r?\n/).slice(-3).join("\n")
5313
+ });
5314
+ if (!result.ok) {
5315
+ return { ok: false, skipped: false, steps: steps2, message: `\`${command}\` failed` };
5316
+ }
5291
5317
  }
5318
+ return { ok: true, skipped: false, steps: steps2 };
5319
+ } finally {
5320
+ cleanups.forEach((fn) => fn());
5292
5321
  }
5293
- return { ok: true, skipped: false, steps: steps2 };
5294
5322
  }
5295
5323
  var agyAdapter = {
5296
5324
  id: "agy",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caddis/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Install, update and diagnose the caddis dev harness across every coding agent you use (Claude Code, agy/Antigravity, and more).",
5
5
  "keywords": [
6
6
  "caddis",