@caddis/cli 0.4.18 → 0.4.20
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/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/scripts/caddis_adhoc.py +530 -0
- package/bundles/antigravity-plugin/scripts/caddis_lanes.py +8 -0
- package/bundles/antigravity-plugin/skills/_registry.md +5 -0
- package/bundles/antigravity-plugin/skills/data-lineage/SKILL.md +32 -0
- package/bundles/antigravity-plugin/skills/hld/SKILL.md +39 -0
- package/bundles/antigravity-plugin/skills/lld/SKILL.md +45 -0
- package/bundles/antigravity-plugin/skills/mmr/SKILL.md +87 -0
- package/bundles/antigravity-plugin/skills/port-to-github/SKILL.md +231 -0
- package/bundles/antigravity-plugin/skills/port-to-github/references/ci-and-credentials.md +66 -0
- package/bundles/antigravity-plugin/skills/port-to-github/references/config.example.json +18 -0
- package/bundles/antigravity-plugin/skills/port-to-github/references/old-host.md +61 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Compare-PortRefs.ps1 +68 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Convert-PortWorkflow.ps1 +92 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Get-PortSource.ps1 +459 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/PortCommon.ps1 +130 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Publish-PortToGitHub.ps1 +156 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Switch-CheckoutOrigin.ps1 +182 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Test-PortReadiness.ps1 +218 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Test-PortWorkflowParses.ps1 +62 -0
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/_registry.md +3 -0
- package/bundles/antigravity-plugin-extras/skills/data-lineage/SKILL.md +158 -0
- package/bundles/antigravity-plugin-extras/skills/data-lineage/scripts/convert_doc_to_confluence.py +1430 -0
- package/bundles/antigravity-plugin-extras/skills/hld/SKILL.md +220 -0
- package/bundles/antigravity-plugin-extras/skills/hld/references/gold-standard-hld.md +428 -0
- package/bundles/antigravity-plugin-extras/skills/hld/scripts/convert_doc_to_confluence.py +1430 -0
- package/bundles/antigravity-plugin-extras/skills/lld/SKILL.md +198 -0
- package/bundles/antigravity-plugin-extras/skills/lld/scripts/convert_doc_to_confluence.py +1430 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/SKILL.md +231 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/references/ci-and-credentials.md +66 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/references/config.example.json +18 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/references/old-host.md +61 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Compare-PortRefs.ps1 +68 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Convert-PortWorkflow.ps1 +92 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Get-PortSource.ps1 +459 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/PortCommon.ps1 +130 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Publish-PortToGitHub.ps1 +156 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Switch-CheckoutOrigin.ps1 +182 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Test-PortReadiness.ps1 +218 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Test-PortWorkflowParses.ps1 +62 -0
- package/bundles/manifest.json +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Multi-Model Review & Runner (MMR) for Caddis.
|
|
3
|
+
|
|
4
|
+
Executes ad-hoc tasks across model lanes (GLM -> Codex -> Claude) in an isolated
|
|
5
|
+
sandbox with automatic rate-limit failover, test gates, cross-vendor review,
|
|
6
|
+
and immediate worktree cleanup. Also performs direct diff reviews.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
21
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
22
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
23
|
+
|
|
24
|
+
EXIT_OK = 0
|
|
25
|
+
EXIT_FAIL = 1
|
|
26
|
+
EXIT_USAGE = 2
|
|
27
|
+
EXIT_CONFIG = 3
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def repo_root(start: Path | None = None) -> Path:
|
|
31
|
+
anchor = Path.cwd() if start is None else start.resolve()
|
|
32
|
+
try:
|
|
33
|
+
raw = subprocess.check_output(
|
|
34
|
+
["git", "-C", str(anchor), "rev-parse", "--show-toplevel"],
|
|
35
|
+
text=True,
|
|
36
|
+
encoding="utf-8",
|
|
37
|
+
errors="replace",
|
|
38
|
+
).strip()
|
|
39
|
+
return Path(raw).resolve()
|
|
40
|
+
except subprocess.CalledProcessError as exc:
|
|
41
|
+
sys.stderr.write(f"[mmr] error: {anchor} is not inside a git repository\n")
|
|
42
|
+
sys.exit(EXIT_CONFIG)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def git_output(repo: Path, *args: str) -> str:
|
|
46
|
+
res = subprocess.run(
|
|
47
|
+
["git", "-C", str(repo), *args],
|
|
48
|
+
capture_output=True,
|
|
49
|
+
text=True,
|
|
50
|
+
encoding="utf-8",
|
|
51
|
+
errors="replace",
|
|
52
|
+
)
|
|
53
|
+
if res.returncode != 0:
|
|
54
|
+
return ""
|
|
55
|
+
return res.stdout.strip()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def check_dirty(repo: Path) -> list[str]:
|
|
59
|
+
output = git_output(repo, "status", "--porcelain")
|
|
60
|
+
lines = [line.strip() for line in output.splitlines() if line.strip()]
|
|
61
|
+
dirty_files: list[str] = []
|
|
62
|
+
for line in lines:
|
|
63
|
+
parts = line.split(maxsplit=1)
|
|
64
|
+
if len(parts) == 2:
|
|
65
|
+
dirty_files.append(parts[1])
|
|
66
|
+
return dirty_files
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def check_keys(repo: Path, keys_file: Path | None = None) -> dict[str, bool]:
|
|
70
|
+
kfile = keys_file if keys_file is not None else Path.home() / ".caddis" / "keys.env"
|
|
71
|
+
file_env: dict[str, str] = {}
|
|
72
|
+
if kfile.is_file():
|
|
73
|
+
try:
|
|
74
|
+
for line in kfile.read_text(encoding="utf-8").splitlines():
|
|
75
|
+
line = line.strip()
|
|
76
|
+
if line and not line.startswith("#") and "=" in line:
|
|
77
|
+
k, _, v = line.partition("=")
|
|
78
|
+
file_env[k.strip()] = v.strip()
|
|
79
|
+
except OSError:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def has_key(var: str) -> bool:
|
|
83
|
+
return bool(os.environ.get(var) or file_env.get(var))
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"deepseek": has_key("DEEPSEEK_API_KEY") or has_key("REVIEW_API_KEY"),
|
|
87
|
+
"glm": has_key("GLM_API_KEY"),
|
|
88
|
+
"openai": has_key("OPENAI_API_KEY") or has_key("CODEX_API_KEY"),
|
|
89
|
+
"anthropic": has_key("ANTHROPIC_API_KEY"),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def find_lanes_script(repo: Path) -> Path:
|
|
94
|
+
sibling = Path(__file__).resolve().with_name("caddis_lanes.py")
|
|
95
|
+
if sibling.is_file():
|
|
96
|
+
return sibling
|
|
97
|
+
in_repo = repo / "scripts" / "caddis_lanes.py"
|
|
98
|
+
if in_repo.is_file():
|
|
99
|
+
return in_repo
|
|
100
|
+
plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT")
|
|
101
|
+
if plugin_root:
|
|
102
|
+
candidate = Path(plugin_root) / "scripts" / "caddis_lanes.py"
|
|
103
|
+
if candidate.is_file():
|
|
104
|
+
return candidate
|
|
105
|
+
raise FileNotFoundError("caddis_lanes.py not found")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def slugify_prompt(prompt: str) -> str:
|
|
109
|
+
first_line = prompt.strip().splitlines()[0] if prompt.strip() else "task"
|
|
110
|
+
cleaned = re.sub(r"[^\w\s-]", "", first_line).strip().lower()
|
|
111
|
+
words = cleaned.split()[:5]
|
|
112
|
+
slug = "-".join(words)[:50].rstrip("-")
|
|
113
|
+
if not slug:
|
|
114
|
+
slug = "task"
|
|
115
|
+
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
116
|
+
return f"{date_str}-{slug}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def synthesize_plan(
|
|
120
|
+
repo: Path,
|
|
121
|
+
prompt: str,
|
|
122
|
+
task_type: str,
|
|
123
|
+
) -> tuple[Path, str, str]:
|
|
124
|
+
slug = slugify_prompt(prompt)
|
|
125
|
+
first_line = prompt.strip().splitlines()[0] if prompt.strip() else "Ad-hoc task"
|
|
126
|
+
title = first_line[:60].strip()
|
|
127
|
+
|
|
128
|
+
adhoc_dir = repo / ".caddis" / "orchestrator" / "adhoc"
|
|
129
|
+
adhoc_dir.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
|
|
131
|
+
plan_path = adhoc_dir / f"{slug}.md"
|
|
132
|
+
plan_content = f"""# Ad-hoc Task: {title}
|
|
133
|
+
|
|
134
|
+
### Phase 1 — {title}
|
|
135
|
+
**Lane:** auto (via caddis_lanes)
|
|
136
|
+
**Type:** {task_type}
|
|
137
|
+
|
|
138
|
+
**Task:**
|
|
139
|
+
{prompt.strip()}
|
|
140
|
+
|
|
141
|
+
**Constraints:**
|
|
142
|
+
- Do NOT install or introduce new third-party dependencies. Use existing repository packages and standard library only.
|
|
143
|
+
- Follow TDD: write or update test assertions first, then implement.
|
|
144
|
+
- Leave all changes in the working directory. Do NOT run git commit, push, or merge.
|
|
145
|
+
|
|
146
|
+
**Done when:**
|
|
147
|
+
Task requirements are met, existing tests pass, and new test assertions verify the implementation.
|
|
148
|
+
"""
|
|
149
|
+
plan_path.write_text(plan_content, encoding="utf-8")
|
|
150
|
+
return plan_path, slug, title
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def adhoc_latest_path(repo: Path) -> Path:
|
|
154
|
+
return repo / ".caddis" / "orchestrator" / "adhoc" / "latest.json"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def read_latest(repo: Path) -> dict | None:
|
|
158
|
+
path = adhoc_latest_path(repo)
|
|
159
|
+
if not path.is_file():
|
|
160
|
+
return None
|
|
161
|
+
try:
|
|
162
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
163
|
+
except (OSError, json.JSONDecodeError):
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def run_adhoc(
|
|
168
|
+
repo: Path,
|
|
169
|
+
prompt: str,
|
|
170
|
+
task_type: str = "backend",
|
|
171
|
+
lane: str | None = None,
|
|
172
|
+
allow_load: bool = False,
|
|
173
|
+
) -> int:
|
|
174
|
+
dirty = check_dirty(repo)
|
|
175
|
+
head_sha = git_output(repo, "rev-parse", "HEAD")
|
|
176
|
+
if dirty:
|
|
177
|
+
print(f"[mmr] Notice: Working tree has {len(dirty)} uncommitted changes.")
|
|
178
|
+
print(f"[mmr] Sandbox branched from HEAD ({head_sha[:7]}). Uncommitted files remain untouched.")
|
|
179
|
+
for d in dirty:
|
|
180
|
+
if d.lower() in prompt.lower():
|
|
181
|
+
print(f"[mmr] Warning: '{d}' has local uncommitted changes. Sandbox runs against committed HEAD.")
|
|
182
|
+
|
|
183
|
+
keys = check_keys(repo)
|
|
184
|
+
if not keys.get("glm") and not keys.get("openai") and not lane:
|
|
185
|
+
print("[mmr] Note: Secondary vendor keys (GLM/Codex) not detected in ~/.caddis/keys.env.")
|
|
186
|
+
print("[mmr] Running on available lanes. Run 'caddis keys' to enable cheap background lanes.")
|
|
187
|
+
|
|
188
|
+
plan_path, slug, title = synthesize_plan(repo, prompt, task_type)
|
|
189
|
+
plan_rel = plan_path.relative_to(repo).as_posix()
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
lanes_script = find_lanes_script(repo)
|
|
193
|
+
except FileNotFoundError as exc:
|
|
194
|
+
sys.stderr.write(f"[mmr] error: {exc}\n")
|
|
195
|
+
return EXIT_CONFIG
|
|
196
|
+
|
|
197
|
+
cmd = [
|
|
198
|
+
sys.executable,
|
|
199
|
+
str(lanes_script),
|
|
200
|
+
"run",
|
|
201
|
+
"--plan",
|
|
202
|
+
plan_rel,
|
|
203
|
+
"--phase",
|
|
204
|
+
"1",
|
|
205
|
+
"--type",
|
|
206
|
+
task_type,
|
|
207
|
+
]
|
|
208
|
+
if lane:
|
|
209
|
+
cmd.extend(["--lane", lane])
|
|
210
|
+
if allow_load:
|
|
211
|
+
cmd.append("--allow-load")
|
|
212
|
+
|
|
213
|
+
print(f"[mmr] Starting isolated task run: {title}")
|
|
214
|
+
res = subprocess.run(cmd, cwd=repo)
|
|
215
|
+
|
|
216
|
+
packet_path = (
|
|
217
|
+
repo
|
|
218
|
+
/ ".caddis"
|
|
219
|
+
/ "orchestrator"
|
|
220
|
+
/ "lane-runs"
|
|
221
|
+
/ slug
|
|
222
|
+
/ "phase-01.attempt-1.json"
|
|
223
|
+
)
|
|
224
|
+
if not packet_path.is_file():
|
|
225
|
+
sys.stderr.write(f"[mmr] error: lane run packet not generated at {packet_path}\n")
|
|
226
|
+
return res.returncode or EXIT_FAIL
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
packet = json.loads(packet_path.read_text(encoding="utf-8"))
|
|
230
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
231
|
+
sys.stderr.write(f"[mmr] error: failed to read packet: {exc}\n")
|
|
232
|
+
return EXIT_FAIL
|
|
233
|
+
|
|
234
|
+
worktree_str = packet.get("worktree")
|
|
235
|
+
changed_files = packet.get("changed_files", [])
|
|
236
|
+
gates_passed = packet.get("gates_passed", False)
|
|
237
|
+
review_info = packet.get("review", {})
|
|
238
|
+
review_status = review_info.get("status", "MISSING")
|
|
239
|
+
assigned_lane = packet.get("lane", "")
|
|
240
|
+
assigned_model = packet.get("model", "")
|
|
241
|
+
adhoc_branch = f"lane/adhoc-{slug}"
|
|
242
|
+
commit_sha = packet.get("base_sha", head_sha)
|
|
243
|
+
|
|
244
|
+
# Teardown worktree immediately to prevent leaks
|
|
245
|
+
if worktree_str:
|
|
246
|
+
wt_path = Path(worktree_str)
|
|
247
|
+
if wt_path.exists():
|
|
248
|
+
if changed_files:
|
|
249
|
+
try:
|
|
250
|
+
subprocess.run(
|
|
251
|
+
["git", "-C", str(wt_path), "add", "--", *changed_files],
|
|
252
|
+
check=True,
|
|
253
|
+
capture_output=True,
|
|
254
|
+
)
|
|
255
|
+
commit_res = subprocess.run(
|
|
256
|
+
["git", "-C", str(wt_path), "commit", "-m", f"adhoc({slug}): {title}"],
|
|
257
|
+
capture_output=True,
|
|
258
|
+
text=True,
|
|
259
|
+
encoding="utf-8",
|
|
260
|
+
)
|
|
261
|
+
if commit_res.returncode == 0:
|
|
262
|
+
commit_sha = git_output(wt_path, "rev-parse", "HEAD")
|
|
263
|
+
except Exception as exc:
|
|
264
|
+
print(f"[mmr] notice: commit in worktree skipped: {exc}")
|
|
265
|
+
|
|
266
|
+
# Point adhoc_branch to the result commit
|
|
267
|
+
subprocess.run(
|
|
268
|
+
["git", "-C", str(repo), "branch", "-f", adhoc_branch, commit_sha],
|
|
269
|
+
capture_output=True,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Cleanly remove the worktree
|
|
273
|
+
subprocess.run(
|
|
274
|
+
["git", "-C", str(repo), "worktree", "remove", "--force", str(wt_path)],
|
|
275
|
+
capture_output=True,
|
|
276
|
+
)
|
|
277
|
+
subprocess.run(["git", "-C", str(repo), "worktree", "prune"], capture_output=True)
|
|
278
|
+
|
|
279
|
+
# Clean up the phase-specific attempt branch
|
|
280
|
+
attempt_branch = packet.get("branch")
|
|
281
|
+
if attempt_branch and attempt_branch != adhoc_branch:
|
|
282
|
+
subprocess.run(
|
|
283
|
+
["git", "-C", str(repo), "branch", "-D", attempt_branch],
|
|
284
|
+
capture_output=True,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
diff_stat = git_output(repo, "diff", "--shortstat", packet.get("base_sha", head_sha), adhoc_branch)
|
|
288
|
+
diff_names = git_output(repo, "diff", "--name-only", packet.get("base_sha", head_sha), adhoc_branch).splitlines()
|
|
289
|
+
|
|
290
|
+
# Check for dependency file modifications
|
|
291
|
+
dep_files = {"package.json", "requirements.txt", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml"}
|
|
292
|
+
modified_deps = [f for f in diff_names if Path(f).name in dep_files]
|
|
293
|
+
|
|
294
|
+
# Save state to latest.json
|
|
295
|
+
state = {
|
|
296
|
+
"slug": slug,
|
|
297
|
+
"title": title,
|
|
298
|
+
"task": prompt,
|
|
299
|
+
"task_type": task_type,
|
|
300
|
+
"branch": adhoc_branch,
|
|
301
|
+
"base_sha": packet.get("base_sha", head_sha),
|
|
302
|
+
"commit_sha": commit_sha,
|
|
303
|
+
"changed_files": diff_names,
|
|
304
|
+
"lane": assigned_lane,
|
|
305
|
+
"model": assigned_model,
|
|
306
|
+
"gates_passed": gates_passed,
|
|
307
|
+
"review_status": review_status,
|
|
308
|
+
"diff_stat": diff_stat.strip(),
|
|
309
|
+
"modified_deps": modified_deps,
|
|
310
|
+
"plan_path": plan_rel,
|
|
311
|
+
"created": datetime.now(timezone.utc).isoformat(),
|
|
312
|
+
}
|
|
313
|
+
adhoc_latest_path(repo).write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8")
|
|
314
|
+
|
|
315
|
+
# Render Verdict Card
|
|
316
|
+
is_success = gates_passed and review_status == "CLEAN" and not modified_deps
|
|
317
|
+
status_label = "PASS" if is_success else "FAIL"
|
|
318
|
+
|
|
319
|
+
card = [
|
|
320
|
+
"┌────────────────────────────────────────────────────────┐",
|
|
321
|
+
f"│ MMR VERDICT: {status_label:<42}│",
|
|
322
|
+
f"│ Task: {title[:48]:<49}│",
|
|
323
|
+
f"│ Branch: {adhoc_branch:<47}│",
|
|
324
|
+
f"│ Model: {assigned_lane or 'auto'} ({assigned_model or 'default'}){' ' * max(0, 39 - len(assigned_lane or '') - len(assigned_model or ''))}│",
|
|
325
|
+
f"│ Diff: {(diff_stat or 'no changes')[:48]:<49}│",
|
|
326
|
+
f"│ Gates: {'PASS' if gates_passed else 'FAILED':<48}│",
|
|
327
|
+
f"│ Review: {review_status:<47}│",
|
|
328
|
+
]
|
|
329
|
+
if modified_deps:
|
|
330
|
+
card.append(f"│ WARN: New dependencies added ({', '.join(modified_deps)}){' ' * max(0, 16 - len(','.join(modified_deps)))}│")
|
|
331
|
+
card.extend(
|
|
332
|
+
[
|
|
333
|
+
"│ │",
|
|
334
|
+
"│ Actions: │",
|
|
335
|
+
"│ /mmr keep Keep branch; offers /ship-pr │",
|
|
336
|
+
"│ /mmr drop Discard branch and delete task files │",
|
|
337
|
+
"└────────────────────────────────────────────────────────┘",
|
|
338
|
+
]
|
|
339
|
+
)
|
|
340
|
+
print("\n" + "\n".join(card) + "\n")
|
|
341
|
+
return EXIT_OK if is_success else EXIT_FAIL
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def keep_adhoc(repo: Path) -> int:
|
|
345
|
+
state = read_latest(repo)
|
|
346
|
+
if state is None:
|
|
347
|
+
print("[mmr] No active task found to keep. Run /mmr \"<task>\" first.")
|
|
348
|
+
return EXIT_USAGE
|
|
349
|
+
|
|
350
|
+
branch = state.get("branch", "")
|
|
351
|
+
if not branch:
|
|
352
|
+
print("[mmr] Active task record has no branch.")
|
|
353
|
+
return EXIT_USAGE
|
|
354
|
+
|
|
355
|
+
branch_sha = git_output(repo, "rev-parse", "--verify", branch)
|
|
356
|
+
if not branch_sha:
|
|
357
|
+
print(f"[mmr] Branch '{branch}' does not exist.")
|
|
358
|
+
return EXIT_FAIL
|
|
359
|
+
|
|
360
|
+
current_branch = git_output(repo, "branch", "--show-current")
|
|
361
|
+
if current_branch in ("main", "master"):
|
|
362
|
+
print(f"[mmr] Kept branch: {branch}")
|
|
363
|
+
print(f"[mmr] Direct merge into '{current_branch}' is disabled to protect trunk.")
|
|
364
|
+
print("Recommended next step: Run /ship-pr to open a pull request and verify CI.")
|
|
365
|
+
return EXIT_OK
|
|
366
|
+
|
|
367
|
+
dirty = check_dirty(repo)
|
|
368
|
+
if dirty:
|
|
369
|
+
print(f"[mmr] Kept branch: {branch}")
|
|
370
|
+
print(f"[mmr] Working tree has {len(dirty)} uncommitted files. Branch was not merged automatically.")
|
|
371
|
+
print(f"To merge when clean: git merge --squash {branch}")
|
|
372
|
+
return EXIT_OK
|
|
373
|
+
|
|
374
|
+
print(f"[mmr] Kept branch: {branch}")
|
|
375
|
+
print(f"To squash onto current branch: git merge --squash {branch}")
|
|
376
|
+
print("Or run /ship-pr to open a pull request.")
|
|
377
|
+
return EXIT_OK
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def drop_adhoc(repo: Path) -> int:
|
|
381
|
+
state = read_latest(repo)
|
|
382
|
+
if state is None:
|
|
383
|
+
print("[mmr] No active task found to drop.")
|
|
384
|
+
return EXIT_USAGE
|
|
385
|
+
|
|
386
|
+
branch = state.get("branch", "")
|
|
387
|
+
slug = state.get("slug", "")
|
|
388
|
+
title = state.get("title", slug)
|
|
389
|
+
|
|
390
|
+
if branch:
|
|
391
|
+
subprocess.run(["git", "-C", str(repo), "branch", "-D", branch], capture_output=True)
|
|
392
|
+
|
|
393
|
+
plan_path = state.get("plan_path")
|
|
394
|
+
if plan_path:
|
|
395
|
+
full_plan = repo / plan_path
|
|
396
|
+
if full_plan.is_file():
|
|
397
|
+
full_plan.unlink(missing_ok=True)
|
|
398
|
+
|
|
399
|
+
run_dir = repo / ".caddis" / "orchestrator" / "lane-runs" / slug
|
|
400
|
+
if run_dir.is_dir():
|
|
401
|
+
shutil.rmtree(run_dir, ignore_errors=True)
|
|
402
|
+
|
|
403
|
+
latest_file = adhoc_latest_path(repo)
|
|
404
|
+
if latest_file.is_file():
|
|
405
|
+
latest_file.unlink(missing_ok=True)
|
|
406
|
+
|
|
407
|
+
print(f"[mmr] Dropped task '{title}'.")
|
|
408
|
+
print(f"[mmr] Branch '{branch}' deleted. Workspace is clean and untouched.")
|
|
409
|
+
return EXIT_OK
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def status_adhoc(repo: Path) -> int:
|
|
413
|
+
keys = check_keys(repo)
|
|
414
|
+
print("=== Multi-Model Review & Runner (MMR) Status ===")
|
|
415
|
+
print("Lane Providers:")
|
|
416
|
+
print(f" Anthropic / Claude : {'Ready' if keys.get('anthropic') else 'Default'}")
|
|
417
|
+
print(f" DeepSeek (Review) : {'Ready' if keys.get('deepseek') else 'Not configured'}")
|
|
418
|
+
print(f" GLM-4 / GLM-5 : {'Ready' if keys.get('glm') else 'Not configured'}")
|
|
419
|
+
print(f" OpenAI / Codex : {'Ready' if keys.get('openai') else 'Not configured'}")
|
|
420
|
+
|
|
421
|
+
state = read_latest(repo)
|
|
422
|
+
if state:
|
|
423
|
+
print("\nActive Task:")
|
|
424
|
+
print(f" Title : {state.get('title')}")
|
|
425
|
+
print(f" Branch : {state.get('branch')}")
|
|
426
|
+
print(f" Model : {state.get('lane')} ({state.get('model')})")
|
|
427
|
+
print(f" Gates : {'PASS' if state.get('gates_passed') else 'FAILED'}")
|
|
428
|
+
print(f" Review : {state.get('review_status')}")
|
|
429
|
+
print(f" Diff : {state.get('diff_stat') or 'none'}")
|
|
430
|
+
else:
|
|
431
|
+
print("\nActive Task: None")
|
|
432
|
+
|
|
433
|
+
branches_output = git_output(repo, "branch", "--list", "lane/adhoc-*")
|
|
434
|
+
adhoc_branches = [b.strip().lstrip("* ") for b in branches_output.splitlines() if b.strip()]
|
|
435
|
+
if adhoc_branches:
|
|
436
|
+
print(f"\nUnmerged Ad-hoc Branches ({len(adhoc_branches)}):")
|
|
437
|
+
for b in adhoc_branches:
|
|
438
|
+
print(f" - {b}")
|
|
439
|
+
|
|
440
|
+
return EXIT_OK
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def review_diff(repo: Path, git_range: str | None = None) -> int:
|
|
444
|
+
# Resolve oss_review.py
|
|
445
|
+
candidates = [
|
|
446
|
+
repo / "scripts" / "oss_review.py",
|
|
447
|
+
repo / ".github" / "tools" / "oss_review.py",
|
|
448
|
+
Path(__file__).resolve().parent / "oss_review.py",
|
|
449
|
+
Path(__file__).resolve().parents[1] / ".github" / "tools" / "oss_review.py",
|
|
450
|
+
]
|
|
451
|
+
tool = next((c for c in candidates if c.is_file()), None)
|
|
452
|
+
if tool is None:
|
|
453
|
+
sys.stderr.write("[mmr] error: oss_review.py not found in repo or tools\n")
|
|
454
|
+
return EXIT_CONFIG
|
|
455
|
+
|
|
456
|
+
cmd = [sys.executable, str(tool), "--cwd", str(repo)]
|
|
457
|
+
if git_range:
|
|
458
|
+
cmd.extend(["--range", git_range])
|
|
459
|
+
|
|
460
|
+
print(f"[mmr] Running Multi-Model Review with {tool.name}...")
|
|
461
|
+
res = subprocess.run(cmd)
|
|
462
|
+
return res.returncode
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def main(argv: list[str] | None = None) -> int:
|
|
466
|
+
args_list = sys.argv[1:] if argv is None else argv
|
|
467
|
+
parser = argparse.ArgumentParser(
|
|
468
|
+
description="Multi-Model Review & Runner (MMR)",
|
|
469
|
+
add_help=True,
|
|
470
|
+
)
|
|
471
|
+
subparsers = parser.add_subparsers(dest="subcommand")
|
|
472
|
+
|
|
473
|
+
# run subcommand
|
|
474
|
+
run_parser = subparsers.add_parser("run", help="Run an ad-hoc task across model lanes")
|
|
475
|
+
run_parser.add_argument("prompt", type=str, help="Task prompt or description")
|
|
476
|
+
run_parser.add_argument(
|
|
477
|
+
"--type",
|
|
478
|
+
choices=["backend", "tests", "docs", "ui", "hard"],
|
|
479
|
+
default="backend",
|
|
480
|
+
help="Task type for chain resolution (default: backend)",
|
|
481
|
+
)
|
|
482
|
+
run_parser.add_argument("--lane", type=str, default=None, help="Start chain at specific lane")
|
|
483
|
+
run_parser.add_argument("--allow-load", action="store_true", help="Allow load/stress wording")
|
|
484
|
+
|
|
485
|
+
# keep subcommand
|
|
486
|
+
subparsers.add_parser("keep", help="Keep the last ad-hoc branch")
|
|
487
|
+
|
|
488
|
+
# drop subcommand
|
|
489
|
+
subparsers.add_parser("drop", help="Discard the last ad-hoc task and clean up")
|
|
490
|
+
|
|
491
|
+
# status subcommand
|
|
492
|
+
subparsers.add_parser("status", help="Show lane readiness and active task status")
|
|
493
|
+
|
|
494
|
+
# review subcommand
|
|
495
|
+
review_parser = subparsers.add_parser("review", help="Run multi-model review on a git diff")
|
|
496
|
+
review_parser.add_argument("--range", type=str, default=None, help="Git range (e.g. origin/main..HEAD)")
|
|
497
|
+
|
|
498
|
+
# Direct shorthand: if first argument is not a known subcommand and not a flag, treat as 'run'
|
|
499
|
+
if args_list and args_list[0] not in ("run", "keep", "drop", "status", "review", "-h", "--help"):
|
|
500
|
+
args_list = ["run", *args_list]
|
|
501
|
+
|
|
502
|
+
if not args_list:
|
|
503
|
+
# Default with no arguments: review the current diff
|
|
504
|
+
args_list = ["review"]
|
|
505
|
+
|
|
506
|
+
parsed = parser.parse_args(args_list)
|
|
507
|
+
repo = repo_root()
|
|
508
|
+
|
|
509
|
+
if parsed.subcommand == "run":
|
|
510
|
+
return run_adhoc(
|
|
511
|
+
repo,
|
|
512
|
+
parsed.prompt,
|
|
513
|
+
task_type=parsed.type,
|
|
514
|
+
lane=parsed.lane,
|
|
515
|
+
allow_load=parsed.allow_load,
|
|
516
|
+
)
|
|
517
|
+
elif parsed.subcommand == "keep":
|
|
518
|
+
return keep_adhoc(repo)
|
|
519
|
+
elif parsed.subcommand == "drop":
|
|
520
|
+
return drop_adhoc(repo)
|
|
521
|
+
elif parsed.subcommand == "status":
|
|
522
|
+
return status_adhoc(repo)
|
|
523
|
+
elif parsed.subcommand == "review":
|
|
524
|
+
return review_diff(repo, git_range=parsed.range)
|
|
525
|
+
|
|
526
|
+
return EXIT_USAGE
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
if __name__ == "__main__":
|
|
530
|
+
sys.exit(main())
|
|
@@ -580,6 +580,10 @@ def oss_review_path() -> Path | None:
|
|
|
580
580
|
|
|
581
581
|
|
|
582
582
|
def run_review(worktree: Path, env: dict, timeout_s: int = 900) -> ReviewResult:
|
|
583
|
+
fake_review = os.environ.get("CADDIS_REVIEW_FAKE")
|
|
584
|
+
if fake_review:
|
|
585
|
+
return ReviewResult(fake_review, 0 if fake_review == "CLEAN" else 1, f"fake review: {fake_review}")
|
|
586
|
+
|
|
583
587
|
path = oss_review_path()
|
|
584
588
|
if path is None:
|
|
585
589
|
return ReviewResult("MISSING", None, "oss_review.py not found")
|
|
@@ -1210,6 +1214,10 @@ def _run_command(
|
|
|
1210
1214
|
"base_sha": wt.base_sha,
|
|
1211
1215
|
}
|
|
1212
1216
|
)
|
|
1217
|
+
wt_plan = wt.path / plan_rel
|
|
1218
|
+
if not wt_plan.exists() and plan.exists():
|
|
1219
|
+
wt_plan.parent.mkdir(parents=True, exist_ok=True)
|
|
1220
|
+
shutil.copy2(plan, wt_plan)
|
|
1213
1221
|
|
|
1214
1222
|
for lane in chain:
|
|
1215
1223
|
model = models[lane]
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
| Ci Cd Pipeline | `ci-cd-pipeline/` | CI/CD pipeline design and implementation for GitHub Actions, Azure DevOps, and general pipeline architecture. Use when creating build pipelines, deployment workflows, quality gates, environment promotion strategies, or automating release processes. |
|
|
40
40
|
| Gh Cli | `gh-cli/` | GitHub CLI operations — issues, PRs, releases, and repo management |
|
|
41
41
|
| Git Commit | `git-commit/` | Create well-structured conventional commit messages following Conventional Commits standard. Use when committing changes, preparing PRs, or generating changelogs. |
|
|
42
|
+
| Port To Github | `port-to-github/` | Move (port) an app or repository to GitHub from ANY source - a Gitea/GitLab/any git server URL, a local folder, a network (UNC) share, or a folder on a remote Windows box reached over WinRM - with or without existing git history. Keeps every branch and tag, verifies by SHA, translates Gitea/Forgejo Actions CI, switches deploy checkouts to GitHub with automatic rollback, and retires the old host safely. Use when the user says "move this app to GitHub", "port X to GitHub", "migrate from Gitea", "put this folder on GitHub", "this app only exists on the server", "import the repo from the prod box", or "cut the deploy over to GitHub". |
|
|
42
43
|
| Safe Cleanup | `safe-cleanup/` | Use BEFORE deleting any directory tree, clearing a scratchpad, removing git worktrees, or "tidying up" generated folders - especially on Windows, where a recursive delete that meets a junction or symlink destroys the link's TARGET. Also use when verifying that a Python venv or node_modules survived a cleanup, because pip check and file counts both report healthy on a half-deleted environment. |
|
|
43
44
|
| Using Git Worktrees | `using-git-worktrees/` | Use when starting feature work that needs isolation from current workspace or before executing implementation plans - creates isolated git worktrees with smart directory selection and safety verification |
|
|
44
45
|
| Windows Deployment | `windows-deployment/` | Deploy FastAPI + React/Vite apps to Windows Server with NSSM services, reverse proxy (IIS or nginx), and git-pull workflow. Use when deploying any web app to a Windows prod server, setting up NSSM services, configuring IIS or nginx reverse proxy, making code environment-aware for dev/prod, or troubleshooting prod deployment issues. |
|
|
@@ -49,7 +50,10 @@
|
|
|
49
50
|
|-------|------|-------------|
|
|
50
51
|
| 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. |
|
|
51
52
|
| Confluence Hla | `confluence-hla/` | Author enterprise High-Level Architecture (HLA) documents in Simplified Technical English and generate Confluence Storage Format XHTML with self-contained PlantUML diagrams, wrapped tables, and zero AI fingerprints. Use when creating high-level architecture designs, architecture documentation for Confluence, or converting markdown architecture to Confluence storage format. |
|
|
53
|
+
| Data Lineage | `data-lineage/` | Generate End-to-End Data Lineage and Telemetry Specification Markdown and Confluence XHTML for fleet observability. |
|
|
52
54
|
| 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. |
|
|
55
|
+
| Hld | `hld/` | Generate High-Level Design (HLD) Markdown documentation and Confluence XHTML with native PlantUML diagrams. |
|
|
56
|
+
| Lld | `lld/` | Generate Lower-Level Design (LLD) Markdown documentation and Confluence XHTML with native PlantUML diagrams. |
|
|
53
57
|
| Tech Explainer | `tech-explainer/` | >- |
|
|
54
58
|
| 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. |
|
|
55
59
|
| Writing Plans | `writing-plans/` | Use when you have a spec or requirements for a multi-step task, before touching code |
|
|
@@ -83,6 +87,7 @@
|
|
|
83
87
|
| Kb | `kb/` | Rebuild the KB index (.caddis/kb/DOC-MAP.md) — create it if missing, index un-indexed notes, report dangling links |
|
|
84
88
|
| Mermaid Db | `mermaid-db/` | Turn a SQL artifact (proc, view, query, .sql file, or table name) into a Mermaid diagram that explains it — git-diffable, saved as .md |
|
|
85
89
|
| Migrate Dir | `migrate-dir/` | Rename this repo's legacy `.claudster/` artifact dir to `.caddis/` — dry-run first, `git mv` to keep history, merges a straggler dir, rewrites live refs. Opt-in, never automatic. |
|
|
90
|
+
| Mmr | `mmr/` | Multi-Model Review & Runner (MMR) — run an ad-hoc task in an isolated multi-lane sandbox with rate-limit failover, test gates, and second-vendor review, or run a fast cross-model review on a diff |
|
|
86
91
|
| Park | `park/` | File a future-work item in the one backlog — .caddis/parking-lot/ — or list what is open |
|
|
87
92
|
| Pr Merge | `pr-merge/` | End-to-end pull request and merge lane — push feature branch, open or update PR, monitor CI to green, merge to default branch, and clean up the feature branch. |
|
|
88
93
|
| Prd | `prd/` | Capture requirements through structured discovery and write a PRD |
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: data-lineage
|
|
3
|
+
description: Generate End-to-End Data Lineage and Telemetry Specification Markdown and Confluence XHTML for fleet observability.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /data-lineage Command
|
|
7
|
+
|
|
8
|
+
Run the End-to-End Data Lineage generation procedure:
|
|
9
|
+
|
|
10
|
+
1. **Target Architecture & Telemetry Discovery:**
|
|
11
|
+
- Scan target application routes, pages, and controllers.
|
|
12
|
+
- Trace API handlers down to physical tables, views, and columns.
|
|
13
|
+
- Identify database server stored procedures, batch scripts, and scheduled SQL Agent jobs that populate each dataset.
|
|
14
|
+
- Cross-reference estate configuration catalogs to identify hostnames, IPs, ports, and synthetic health probe endpoints.
|
|
15
|
+
|
|
16
|
+
2. **Author Canonical Markdown Document:**
|
|
17
|
+
- Create document at `docs/<app>/<app>-data-lineage.md`.
|
|
18
|
+
- Adhere strictly to the standard 5-section lineage structure:
|
|
19
|
+
1. Physical Estate & Host Infrastructure (servers, IPs, ports, database instances, access methods).
|
|
20
|
+
2. End-to-End Dependency & Impact Flowchart (vibrant Mermaid flowchart from feeds down to UI surfaces).
|
|
21
|
+
3. Comprehensive 7-Column Lineage Matrix (`App`, `UI Page`, `API Route`, `Dataset`, `Producer`, `Database`, `Physical Host`).
|
|
22
|
+
4. Stored Procedures & Producer Execution Catalog (schedules, triggers, parameters, source and target tables).
|
|
23
|
+
5. Telemetry Ingestion Contract (machine-readable JSON target graph).
|
|
24
|
+
- Use ASD-STE100 Simplified Technical English: active voice, short sentences, zero em-dashes.
|
|
25
|
+
|
|
26
|
+
3. **Convert to Confluence Storage Format XHTML:**
|
|
27
|
+
- Run the PlantUML-enabled converter:
|
|
28
|
+
```powershell
|
|
29
|
+
python scripts/convert_doc_to_confluence.py docs/<app>/<app>-data-lineage.md --output docs/<app>/<app>-data-lineage.html --diagram-type plantuml
|
|
30
|
+
```
|
|
31
|
+
- Verify that all diagrams convert to native `<ac:structured-macro ac:name="plantuml">` blocks with scale 1.2, natural splines, auto-wrapped edge labels, crimson red borders on impacted components, and embedded Creole legends.
|
|
32
|
+
- Verify that output validates as 100% well-formed XML.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hld
|
|
3
|
+
description: Generate High-Level Design (HLD) Markdown documentation and Confluence XHTML with native PlantUML diagrams.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /hld Command
|
|
7
|
+
|
|
8
|
+
Run the High-Level Design (HLD) generation procedure:
|
|
9
|
+
|
|
10
|
+
1. **Research & Scope Analysis:**
|
|
11
|
+
- Review target application architecture, user navigation surfaces, and primary databases.
|
|
12
|
+
- Enforce HLD boundaries: exclude internal API routes, low-level stored procedures, and detailed ETL scripts.
|
|
13
|
+
- Use high-level reporting summary tables and domain entities only.
|
|
14
|
+
|
|
15
|
+
2. **Component & Interface Classification Interview (Grill-Me):**
|
|
16
|
+
- **MANDATORY GATE:** Present discovered systems (`SYS_xx`) and interfaces (`INT_xx`) to the user.
|
|
17
|
+
- Ask the user to confirm/clarify:
|
|
18
|
+
- Which systems/components are Existing fleet infrastructure (`Impacted = No`, Blue `#1D4ED8`) vs New / Impacted (`Impacted = Yes`, Red `#DC2626`)?
|
|
19
|
+
- Which interfaces are Existing Connectivity (Green `#16A34A`) vs New Connectivity (Red `#DC2626`)?
|
|
20
|
+
- Formulate recommendations from codebase discovery and seek user sign-off in a single interactive turn.
|
|
21
|
+
|
|
22
|
+
3. **Author Canonical Markdown Document:**
|
|
23
|
+
- Create document at `docs/<app>/<app>-hld.md`.
|
|
24
|
+
- Adhere strictly to the standard HLD structure.
|
|
25
|
+
- **Do not synthesize or fabricate "Target Architecture", "Target State Roadmap", or "Security Topology" sections/diagrams** when there are no notes mentioning future architecture and the user does not request them. Only include them when explicitly requested by the user or specified in existing project notes as future work.
|
|
26
|
+
- Place the core architecture diagram in Section 4 titled `### 4.1 Solution Design and Architecture` under `## 4. Solution Architecture Landscape`. Do not use `System Topology Architecture Diagram` or place it in Section 2.
|
|
27
|
+
- Represent domain data models in Section 5 (`### 5.2 Domain Entity Model`) using Mermaid `classDiagram` or `erDiagram` syntax, converted natively to PlantUML.
|
|
28
|
+
- Ensure Mermaid diagrams depict automated schedulers as system participants, not human actors.
|
|
29
|
+
- Use colorful, professional Mermaid diagrams with explicit `classDef` styling palettes (no raw yellow boxes).
|
|
30
|
+
- Use ASD-STE100 Simplified Technical English: active voice, short sentences, zero em-dashes.
|
|
31
|
+
|
|
32
|
+
4. **Convert to Confluence Storage Format XHTML:**
|
|
33
|
+
- Run the PlantUML-enabled converter:
|
|
34
|
+
```powershell
|
|
35
|
+
python scripts/convert_doc_to_confluence.py docs/<app>/<app>-hld.md --output docs/<app>/<app>-hld.html --diagram-type plantuml
|
|
36
|
+
```
|
|
37
|
+
- Verify that all diagrams are converted to native `<ac:structured-macro ac:name="plantuml">` blocks with scale 1.2, natural splines, auto-wrapped edge labels, crimson red borders on impacted components, and embedded Creole legends.
|
|
38
|
+
- Ensure multi-stage operational flows use horizontal side-by-side stages (`flowchart LR`) and architecture topologies follow clean 3-tier hierarchies (`flowchart TD`).
|
|
39
|
+
- Verify that output validates as well-formed XML.
|