@caddis/cli 0.4.29 → 0.4.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/bundles/antigravity-plugin/caddis_statusline.py +0 -1
  2. package/bundles/antigravity-plugin/mcp_ping_server.py +1 -1
  3. package/bundles/antigravity-plugin/plugin.json +1 -1
  4. package/bundles/antigravity-plugin/scripts/caddis_adhoc.py +301 -66
  5. package/bundles/antigravity-plugin/scripts/caddis_exit.py +15 -0
  6. package/bundles/antigravity-plugin/scripts/caddis_frontmatter.py +145 -0
  7. package/bundles/antigravity-plugin/scripts/caddis_gate.py +33 -10
  8. package/bundles/antigravity-plugin/scripts/caddis_governor.py +5 -2
  9. package/bundles/antigravity-plugin/scripts/caddis_lanes.py +13 -1
  10. package/bundles/antigravity-plugin/scripts/caddis_migrate_dir.py +1 -1
  11. package/bundles/antigravity-plugin/scripts/caddis_spawn.py +2 -2
  12. package/bundles/antigravity-plugin/scripts/caddis_statusline.py +0 -1
  13. package/bundles/antigravity-plugin/scripts/caddis_tidy.py +9 -14
  14. package/bundles/antigravity-plugin/scripts/claudster_doctor.py +66 -0
  15. package/bundles/antigravity-plugin/scripts/convert_doc_to_confluence.py +1430 -0
  16. package/bundles/antigravity-plugin/scripts/new-machine.ps1 +1 -1
  17. package/bundles/antigravity-plugin/scripts/oss_ask.py +6 -5
  18. package/bundles/antigravity-plugin/scripts/oss_model.py +1 -1
  19. package/bundles/antigravity-plugin/scripts/oss_review.py +84 -15
  20. package/bundles/antigravity-plugin/scripts/secret_filter.py +316 -0
  21. package/bundles/antigravity-plugin/scripts/setup_project_ai.py +9 -4
  22. package/bundles/antigravity-plugin/skills/_registry.md +5 -3
  23. package/bundles/antigravity-plugin/skills/catchup/SKILL.md +37 -2
  24. package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +1 -1
  25. package/bundles/antigravity-plugin/skills/data-lineage/SKILL.md +1 -1
  26. package/bundles/antigravity-plugin/skills/digress/SKILL.md +1 -1
  27. package/bundles/antigravity-plugin/skills/handoff/SKILL.md +8 -1
  28. package/bundles/antigravity-plugin/skills/hld/SKILL.md +1 -1
  29. package/bundles/antigravity-plugin/skills/implement/SKILL.md +3 -1
  30. package/bundles/antigravity-plugin/skills/install-statusline/SKILL.md +105 -0
  31. package/bundles/antigravity-plugin/skills/lld/SKILL.md +1 -1
  32. package/bundles/antigravity-plugin/skills/mmr/SKILL.md +23 -14
  33. package/bundles/antigravity-plugin/skills/resume/SKILL.md +2 -32
  34. package/bundles/antigravity-plugin/skills/review/SKILL.md +2 -96
  35. package/bundles/antigravity-plugin/skills/review-pr/SKILL.md +100 -0
  36. package/bundles/antigravity-plugin/skills/statusline/SKILL.md +2 -101
  37. package/bundles/antigravity-plugin-extras/plugin.json +1 -1
  38. package/bundles/antigravity-plugin-extras/skills/cross-review/SKILL.md +3 -3
  39. package/bundles/antigravity-plugin-extras/skills/golden-plan/SKILL.md +1 -1
  40. package/bundles/codex/.codex/config.toml.example +0 -16
  41. package/bundles/codex/AGENTS.md +2 -2
  42. package/bundles/manifest.json +3 -3
  43. package/dist/claude-deepseek.js +1 -1
  44. package/dist/claude-glm.js +1 -1
  45. package/dist/claude-oss.js +1 -1
  46. package/dist/cli.js +1 -1
  47. package/package.json +1 -1
@@ -37,6 +37,14 @@ import subprocess
37
37
  import sys
38
38
  from pathlib import Path
39
39
 
40
+ try:
41
+ import caddis_frontmatter
42
+ except ModuleNotFoundError as exc:
43
+ if exc.name != "caddis_frontmatter":
44
+ raise
45
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "claude-harness" / "scripts"))
46
+ import caddis_frontmatter
47
+
40
48
  if hasattr(sys.stdout, "reconfigure"):
41
49
  sys.stdout.reconfigure(encoding="utf-8", errors="replace")
42
50
  sys.stderr.reconfigure(encoding="utf-8", errors="replace")
@@ -145,16 +153,7 @@ def gate_lane(plan: Path, phase: int) -> int:
145
153
  # ── gate: verdict-gate ──────────────────────────────────────────────────────
146
154
 
147
155
  def _frontmatter(p: Path) -> dict:
148
- t = p.read_text(encoding="utf-8", errors="ignore")
149
- if not t.startswith("---"):
150
- return {}
151
- end = t.find("\n---", 3)
152
- out = {}
153
- for line in t[3:end if end > 0 else 400].splitlines():
154
- if ":" in line:
155
- k, _, v = line.partition(":")
156
- out[k.strip()] = v.strip()
157
- return out
156
+ return caddis_frontmatter.parse(p.read_text(encoding="utf-8", errors="ignore"))
158
157
 
159
158
 
160
159
  def _covers(phase_field: str, n: int) -> bool:
@@ -602,6 +601,22 @@ def gate_review_trigger(repo_root: Path, rev_range: str | None = None) -> int:
602
601
  return EXIT_NOTE
603
602
 
604
603
 
604
+ def gate_docs_check(root: Path) -> int:
605
+ """Report every typed .caddis Markdown document with an invalid header."""
606
+ failures = 0
607
+ for path in sorted((root / ".caddis").rglob("*.md")):
608
+ relative = path.relative_to(root).as_posix()
609
+ if not caddis_frontmatter.doc_in_scope(relative, root):
610
+ continue
611
+ error = caddis_frontmatter.validate(
612
+ path.read_text(encoding="utf-8"), caddis_frontmatter.ALLOWED_TYPES
613
+ )
614
+ if error:
615
+ print(f"{relative}: {error}")
616
+ failures += 1
617
+ return EXIT_BLOCKED if failures else EXIT_OK
618
+
619
+
605
620
  def main(argv: list[str] | None = None) -> int:
606
621
  ap = argparse.ArgumentParser(description="caddis machine gates")
607
622
  sub = ap.add_subparsers(dest="cmd", required=True)
@@ -625,9 +640,17 @@ def main(argv: list[str] | None = None) -> int:
625
640
  hc = sub.add_parser("handover-check")
626
641
  hc.add_argument("--doc", required=True)
627
642
  hc.add_argument("--repo-root", default=".")
643
+ dc = sub.add_parser("docs-check")
644
+ dc.add_argument("--root", default=".")
628
645
  a = ap.parse_args(argv)
629
646
 
630
647
  plan = Path(getattr(a, "plan", "") or ".")
648
+ if a.cmd == "docs-check":
649
+ try:
650
+ return gate_docs_check(Path(a.root).resolve())
651
+ except Exception as exc:
652
+ sys.stderr.write(f"[caddis-gate] docs-check cannot evaluate: {type(exc).__name__}: {exc}\n")
653
+ return EXIT_BLOCKED # CI must not pass without completing the scan.
631
654
  try:
632
655
  if a.cmd == "lane-check":
633
656
  return gate_lane(plan, a.phase)
@@ -55,8 +55,11 @@ import sys
55
55
  from dataclasses import dataclass
56
56
  from pathlib import Path
57
57
 
58
- EXIT_USAGE = 2
59
- EXIT_CONFIG = 3
58
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
59
+ import caddis_exit # noqa: E402
60
+
61
+ EXIT_USAGE = caddis_exit.ERROR
62
+ EXIT_CONFIG = caddis_exit.NOT_RUN
60
63
  SCOPES = ("session", "lane")
61
64
  MAX_CPU_PERCENT = 100
62
65
  MAX_MEMORY_MB = 1_048_576
@@ -20,11 +20,23 @@ from dataclasses import dataclass
20
20
  from datetime import datetime, timezone
21
21
  from pathlib import Path
22
22
 
23
+ try:
24
+ import caddis_exit
25
+ except ModuleNotFoundError as exc:
26
+ if exc.name != "caddis_exit":
27
+ raise
28
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "claude-harness" / "scripts"))
29
+ import caddis_exit
30
+
23
31
  if hasattr(sys.stdout, "reconfigure"):
24
32
  sys.stdout.reconfigure(encoding="utf-8", errors="replace")
25
33
  sys.stderr.reconfigure(encoding="utf-8", errors="replace")
26
34
 
27
- EXIT_OK, EXIT_FAIL, EXIT_USAGE, EXIT_CONFIG, EXIT_ALL_OUT = 0, 1, 2, 3, 4
35
+ EXIT_OK = caddis_exit.CLEAN
36
+ EXIT_FAIL = caddis_exit.BLOCKED
37
+ EXIT_USAGE = caddis_exit.ERROR
38
+ EXIT_CONFIG = caddis_exit.NOT_RUN
39
+ EXIT_ALL_OUT = caddis_exit.NOT_RUN
28
40
  RESULT_OK = "OK"
29
41
  RESULT_LANE_FAILED = "LANE-FAILED"
30
42
  RESULT_OUT_OF_BUDGET = "OUT-OF-BUDGET"
@@ -45,7 +45,7 @@ NEW_DIR = ".caddis"
45
45
  OLD_DIR = ".claudster"
46
46
 
47
47
  #: Append-only JSONL logs: safe to concatenate when both dirs carry one (legacy lines first).
48
- JSONL_MERGE_NAMES = frozenset({"usage-log.jsonl", "agent-log.jsonl", "memory.jsonl"})
48
+ JSONL_MERGE_NAMES = frozenset({"usage-log.jsonl", "agent-log.jsonl"})
49
49
 
50
50
  #: Files whose CONTENT may name the old dir and is read live after the migration. Historical
51
51
  #: artifacts (plans, handoffs, past reviews) are deliberately NOT rewritten — they are a record of
@@ -273,7 +273,7 @@ def read_context_pct(repo_root: Path) -> tuple[float | None, str]:
273
273
  age = max(0, int(time.time()) - int(raw.get("at", 0)))
274
274
  except Exception:
275
275
  return None, ("no runtime context figure at "
276
- + f"{ART}/context-window.json — run `/caddis:statusline` to install the "
276
+ + f"{ART}/context-window.json — run `/caddis:install-statusline` to install the "
277
277
  "status line that writes it")
278
278
  if age > CONTEXT_MAX_AGE_S:
279
279
  return pct, f"runtime figure, STALE by {age}s (a lower bound — the real number is >= this)"
@@ -386,7 +386,7 @@ def _gate_context(p: Preflight, repo_root: Path, self_reported: float | None) ->
386
386
  if pct is None:
387
387
  p.refusals.append(
388
388
  "no context figure, so the 95% gate cannot be applied — and a gate that defaults to "
389
- "proceed is not a gate. Either run `/caddis:statusline` (its status line caches the "
389
+ "proceed is not a gate. Either run `/caddis:install-statusline` (its status line caches the "
390
390
  "runtime figure for this repo), or pass `--context-pct <n>` with the number your "
391
391
  "harness reports. A self-reported figure is accepted and recorded as such.")
392
392
  return
@@ -615,7 +615,6 @@ def render_agy(data: dict, cfg: dict) -> str:
615
615
  used = as_int(cw.get("total_input_tokens"))
616
616
  max_tokens = as_int(cw.get("context_window_size")) or 1_048_576
617
617
  pct = float(as_int(cw.get("used_percentage")))
618
- record_context(cwd, pct)
619
618
  parts.append(context_segment(c, cfg, used, max_tokens, pct))
620
619
  loud = cached_segment(c, cfg, cached)
621
620
  if loud:
@@ -38,6 +38,14 @@ import sys
38
38
  from dataclasses import dataclass, field
39
39
  from pathlib import Path
40
40
 
41
+ try:
42
+ import caddis_frontmatter
43
+ except ModuleNotFoundError as exc:
44
+ if exc.name != "caddis_frontmatter":
45
+ raise
46
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "claude-harness" / "scripts"))
47
+ import caddis_frontmatter
48
+
41
49
  TERMINAL_STATUSES = {"done", "superseded", "shipped", "implemented"}
42
50
 
43
51
  # Legacy prompts written before the frontmatter convention existed (2026-07-23). They were
@@ -95,20 +103,7 @@ PARKING_LOT_MAX_BYTES = 20_000
95
103
 
96
104
 
97
105
  def _parse_frontmatter(text: str) -> dict:
98
- text = text.lstrip("")
99
- if not text.startswith("---"):
100
- return {}
101
- lines = text.splitlines()
102
- if lines[0].strip() != "---":
103
- return {}
104
- fm: dict[str, str] = {}
105
- for line in lines[1:]:
106
- if line.strip() == "---":
107
- break
108
- if ":" in line:
109
- k, _, v = line.partition(":")
110
- fm[k.strip()] = v.strip()
111
- return fm
106
+ return caddis_frontmatter.parse(text)
112
107
 
113
108
 
114
109
  def _phases_all_done(text: str) -> bool:
@@ -15,11 +15,14 @@ Usage:
15
15
  from __future__ import annotations
16
16
 
17
17
  import argparse
18
+ import importlib.util
18
19
  import json
19
20
  import os
21
+ import re
20
22
  import shutil
21
23
  import subprocess
22
24
  import sys
25
+ import time
23
26
  from datetime import datetime, timezone
24
27
  from pathlib import Path
25
28
 
@@ -97,6 +100,28 @@ def rules_findings(dest: Path) -> list[str]:
97
100
  return out
98
101
 
99
102
 
103
+ def has_document_frontmatter_rule(dest: Path) -> bool:
104
+ """True when root AGENTS.md puts type: in its Document frontmatter section."""
105
+ path = Path(dest) / "AGENTS.md"
106
+ try:
107
+ lines = path.read_text(encoding="utf-8").splitlines()
108
+ except OSError:
109
+ return False
110
+ section = False
111
+ for line in lines:
112
+ if re.match(r"^\s*(?:#{1,6}\s+|[-*]\s+\*\*)Document frontmatter(?:\*\*)?\s*:?.*$", line):
113
+ section = True
114
+ if "type:" in line:
115
+ return True
116
+ continue
117
+ if section and ("type:" in line or re.match(r"^\s*[-*]\s+\*\*type\*\*\s*:", line)):
118
+ return True
119
+ if section and (re.match(r"^#{1,6}\s+", line) or
120
+ re.match(r"^\s*[-*]\s+\*\*[^*]+\*\*\s*:", line)):
121
+ section = False
122
+ return False
123
+
124
+
100
125
  def oversize_rules_files(dest: Path, budget: int = AGENTS_MD_BUDGET) -> list[tuple[str, int]]:
101
126
  """Always-loaded rules files (AGENTS.md, or CLAUDE.md pre-migration) over the line budget."""
102
127
  dest = Path(dest)
@@ -287,6 +312,36 @@ def nudge_line(dest: Path) -> str | None:
287
312
  return f"[caddis] {head}{more}"
288
313
 
289
314
 
315
+ def hook_errors(dest: Path) -> str:
316
+ """Recent hook failures, counted by hook. Empty when the ledger is clean."""
317
+ scripts = Path(__file__).resolve().parent
318
+ hook_log_path = scripts / "hook_log.py"
319
+ if not hook_log_path.is_file(): # source checkout; installed copies are siblings
320
+ hook_log_path = scripts.parent / "claude-harness" / "scripts" / "hook_log.py"
321
+ try:
322
+ spec = importlib.util.spec_from_file_location("caddis_hook_log", hook_log_path)
323
+ if spec is None or spec.loader is None:
324
+ return ""
325
+ hook_log = importlib.util.module_from_spec(spec)
326
+ spec.loader.exec_module(hook_log)
327
+ if not hook_log.summarise(str(dest), days=7):
328
+ return ""
329
+ cutoff = time.strftime("%Y-%m-%d", time.localtime(time.time() - 7 * 86400))
330
+ counts: dict[str, int] = {}
331
+ with (dest / ".caddis" / "hook-errors.jsonl").open(encoding="utf-8", errors="replace") as fh:
332
+ for line in fh:
333
+ try:
334
+ row = json.loads(line)
335
+ except (ValueError, TypeError):
336
+ continue
337
+ if isinstance(row, dict) and str(row.get("ts", ""))[:10] >= cutoff:
338
+ hook = str(row.get("hook", "?"))
339
+ counts[hook] = counts.get(hook, 0) + 1
340
+ return ", ".join(f"{hook} x{count}" for hook, count in sorted(counts.items()))
341
+ except Exception:
342
+ return "" # a damaged ledger must not break the doctor
343
+
344
+
290
345
  # ── report ───────────────────────────────────────────────────────────────────
291
346
  def run(dest: Path, quiet: bool) -> int:
292
347
  if quiet: # SessionStart mode: only the nudge, never non-zero
@@ -316,6 +371,11 @@ def run(dest: Path, quiet: bool) -> int:
316
371
  root = dest / "AGENTS.md"
317
372
  print(f" OK root AGENTS.md {'present' if root.is_file() else 'ABSENT (not migrated yet?)'}; shims are shims")
318
373
 
374
+ if has_document_frontmatter_rule(dest):
375
+ print(" OK AGENTS.md carries the document-header rule")
376
+ else:
377
+ print(" WARN AGENTS.md carries the document-header rule: missing Document frontmatter section with type:")
378
+
319
379
  print("-- maintenance signals (deterministic — read-only)")
320
380
  sig = maintenance_signals(dest)
321
381
  if sig:
@@ -324,6 +384,12 @@ def run(dest: Path, quiet: bool) -> int:
324
384
  else:
325
385
  print(" none")
326
386
 
387
+ errors = hook_errors(dest)
388
+ if errors:
389
+ print(f" WARN hook errors (7 days): {errors}")
390
+ else:
391
+ print(" OK hook errors (7 days): 0")
392
+
327
393
  print("-- install registry")
328
394
  reg = _user_scope_path("installs.json")
329
395
  if reg.is_file():