@caddis/cli 0.4.38 → 0.4.41
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_gate.py +45 -10
- package/bundles/antigravity-plugin/scripts/caddis_inventory.py +2 -0
- package/bundles/antigravity-plugin/scripts/caddis_lanes.py +7 -1
- package/bundles/antigravity-plugin/scripts/caddis_workstreams.py +15 -1
- package/bundles/antigravity-plugin/scripts/claudster_doctor.py +253 -11
- package/bundles/antigravity-plugin/settings.template.json +37 -0
- package/bundles/manifest.json +2 -2
- package/dist/cli.js +251 -130
- package/package.json +1 -1
|
@@ -108,6 +108,18 @@ _LANES_RUN = r'python\s+"?\$\{CLAUDE_PLUGIN_ROOT\}/scripts/caddis_lanes\.py"?\s+
|
|
|
108
108
|
|
|
109
109
|
# ── gate: lane-check ────────────────────────────────────────────────────────
|
|
110
110
|
|
|
111
|
+
_QUOTED = re.compile(r"\"[^\"]*\"|'[^']*'")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _has_print_flag(cmd: str) -> bool:
|
|
115
|
+
"""True when the command carries a real -p/--print flag.
|
|
116
|
+
|
|
117
|
+
The quoted prompt is blanked first: a prompt that merely contains the text " -p " must not
|
|
118
|
+
count, or an interactive child passes the gate (R3 cross-review, 2026-09-30).
|
|
119
|
+
"""
|
|
120
|
+
return bool(re.search(r"(?:^|\s)(?:-p|--print)(?:\s|$)", _QUOTED.sub('""', cmd)))
|
|
121
|
+
|
|
122
|
+
|
|
111
123
|
def gate_lane(plan: Path, phase: int) -> int:
|
|
112
124
|
if not plan.is_file():
|
|
113
125
|
return _degrade(f"no plan at {plan}")
|
|
@@ -157,12 +169,12 @@ def gate_lane(plan: Path, phase: int) -> int:
|
|
|
157
169
|
f"[caddis-gate] phase {phase}'s direct command names Phase {named.group(1)}: {cmd}\n"
|
|
158
170
|
" The lane would run another phase's work. Refusing to run it.\n")
|
|
159
171
|
return EXIT_MALFORMED
|
|
160
|
-
if cmd.startswith("agy") and not
|
|
172
|
+
if cmd.startswith("agy") and not _has_print_flag(cmd):
|
|
161
173
|
sys.stderr.write(
|
|
162
174
|
f"[caddis-gate] phase {phase}'s direct agy command has no -p/--print: {cmd}\n"
|
|
163
175
|
" The child would not run headless. Refusing to run it.\n")
|
|
164
176
|
return EXIT_MALFORMED
|
|
165
|
-
elif not
|
|
177
|
+
elif not _has_print_flag(cmd):
|
|
166
178
|
sys.stderr.write(
|
|
167
179
|
f"[caddis-gate] phase {phase}'s launch command has no -p/--print: {cmd}\n"
|
|
168
180
|
" Without it the child is not marked headless, reads the same Lane line, and spawns\n"
|
|
@@ -496,14 +508,37 @@ def gate_handover_check(doc: Path, repo_root: Path, max_chars: int | None = None
|
|
|
496
508
|
return EXIT_BLOCKED
|
|
497
509
|
|
|
498
510
|
missing: list[str] = []
|
|
499
|
-
|
|
500
|
-
#
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if not
|
|
506
|
-
|
|
511
|
+
try:
|
|
512
|
+
# One rule for both gates: `caddis_spawn.check_document` already treats a bare filename
|
|
513
|
+
# that matches no tracked file as a note (probably another repository's file), and one
|
|
514
|
+
# that matches tracked files as a block that names the full path. Two gates giving
|
|
515
|
+
# different answers to the same question is the defect this replaces.
|
|
516
|
+
here = str(Path(__file__).resolve().parent)
|
|
517
|
+
if here not in sys.path:
|
|
518
|
+
sys.path.insert(0, here)
|
|
519
|
+
import caddis_spawn
|
|
520
|
+
findings = caddis_spawn.check_document(text, repo_root)
|
|
521
|
+
missing = sorted(set(findings.missing_paths))
|
|
522
|
+
for name in findings.unresolved_names:
|
|
523
|
+
sys.stderr.write(
|
|
524
|
+
f"[caddis-gate] note: `{name}` is a bare filename that matches no tracked file here; "
|
|
525
|
+
"not checked\n")
|
|
526
|
+
except Exception as exc:
|
|
527
|
+
# Degrade to the plain existence rule, but say why: a bug inside the shared helper must
|
|
528
|
+
# not hide behind the fallback (an ImportError is the expected, quiet case).
|
|
529
|
+
if not isinstance(exc, ImportError):
|
|
530
|
+
sys.stderr.write(
|
|
531
|
+
f"[caddis-gate] note: shared path check failed ({type(exc).__name__}: {exc}); "
|
|
532
|
+
"using the plain existence rule\n")
|
|
533
|
+
missing = []
|
|
534
|
+
for raw in sorted(set(_DOC_PATH.findall(text))):
|
|
535
|
+
# A backtick often wraps a whole command; the path is the token that looks like one.
|
|
536
|
+
cand = next((tok for tok in raw.split() if "/" in tok or tok.endswith(".md")), raw)
|
|
537
|
+
cand = cand.strip("(),;:'\"")
|
|
538
|
+
if cand.startswith(("http://", "https://", "~", "$")) or ".." in cand:
|
|
539
|
+
continue
|
|
540
|
+
if not (repo_root / cand).exists():
|
|
541
|
+
missing.append(cand)
|
|
507
542
|
|
|
508
543
|
stale: list[dict] = []
|
|
509
544
|
try:
|
|
@@ -263,6 +263,8 @@ def stale_artifacts(root: Path) -> list[dict]:
|
|
|
263
263
|
target = root / m
|
|
264
264
|
if not target.is_file():
|
|
265
265
|
continue
|
|
266
|
+
if not _run(["git", "ls-files", "--", m], root):
|
|
267
|
+
continue # untracked or git-ignored: rebuilt locally, so an old commit says nothing
|
|
266
268
|
t_art = _run(["git", "log", "-1", "--format=%ct", "--", m], root)
|
|
267
269
|
t_gen = _run(["git", "log", "-1", "--format=%ct", "--", f"scripts/{script.name}"], root)
|
|
268
270
|
if not t_art or not t_gen:
|
|
@@ -992,7 +992,13 @@ def next_lane(
|
|
|
992
992
|
|
|
993
993
|
|
|
994
994
|
def _ci_watch_block(output: str) -> dict[str, str] | None:
|
|
995
|
-
"""Read the last complete watch report, ignoring an echoed prompt template.
|
|
995
|
+
"""Read the last complete watch report, ignoring an echoed prompt template.
|
|
996
|
+
|
|
997
|
+
The block must hold exactly RESULT, CHECKS, MERGE_STATE and FAILING, RESULT first. A reordered
|
|
998
|
+
or extended block returns None, and the caller then falls back to `gh` without saying why:
|
|
999
|
+
that is safe (the watch prompt pins the order) but invisible, so keep the prompt and this
|
|
1000
|
+
reader in step.
|
|
1001
|
+
"""
|
|
996
1002
|
fields: dict[str, str] = {}
|
|
997
1003
|
reports: list[dict[str, str]] = []
|
|
998
1004
|
for line in output.splitlines():
|
|
@@ -259,6 +259,7 @@ def render_index(root: Path, session_id: str | None, branch: str | None) -> str:
|
|
|
259
259
|
rows.append((at.timestamp(), f"- {label} ({branch_label}, {_age(at, now)}): {_next_step(content)}"))
|
|
260
260
|
except (ValueError, KeyError, TypeError, AttributeError, OSError, UnicodeError):
|
|
261
261
|
continue
|
|
262
|
+
unnamed: list[tuple[datetime, str, str, str]] = []
|
|
262
263
|
states = artifact / "session-state"
|
|
263
264
|
if states.exists() and not states.resolve().is_relative_to(artifact.resolve()):
|
|
264
265
|
raise ValueError("session-state directory escapes artifact root")
|
|
@@ -277,9 +278,22 @@ def render_index(root: Path, session_id: str | None, branch: str | None) -> str:
|
|
|
277
278
|
asked = request.group(1).strip() if request else ""
|
|
278
279
|
note = _single_line(asked, 80) if asked and not asked.startswith("<") else "recent session"
|
|
279
280
|
branch_label = _single_line(state_branch.group(1), 40) if state_branch else "unknown branch"
|
|
280
|
-
|
|
281
|
+
if session_id and path.stem == session_id:
|
|
282
|
+
rows.append((modified.timestamp(), f"- this session, no relay yet ({branch_label}): {note}"))
|
|
283
|
+
else:
|
|
284
|
+
unnamed.append((modified, label, branch_label, note))
|
|
281
285
|
except (OSError, UnicodeError):
|
|
282
286
|
continue
|
|
287
|
+
if unnamed:
|
|
288
|
+
# One line for every other session without a relay: a forked, peer or never-handed-off
|
|
289
|
+
# session is repeated text at every session start and buries the real workstreams.
|
|
290
|
+
unnamed.sort(key=lambda item: (-item[0].timestamp(), item[1]))
|
|
291
|
+
newest, label, _branch_label, note = unnamed[0]
|
|
292
|
+
count = len(unnamed)
|
|
293
|
+
noun = "session" if count == 1 else "sessions"
|
|
294
|
+
rows.append((newest.timestamp(),
|
|
295
|
+
f"- {count} other {noun} without a relay (newest {label}, {_age(newest, now)}): "
|
|
296
|
+
f"{note}; see .caddis/session-state/"))
|
|
283
297
|
rows.sort(key=lambda item: (-item[0], item[1]))
|
|
284
298
|
heading = _INDEX_MARKER + "# Workstream index\n"
|
|
285
299
|
if session_id or branch:
|
|
@@ -23,9 +23,21 @@ import shutil
|
|
|
23
23
|
import subprocess
|
|
24
24
|
import sys
|
|
25
25
|
import time
|
|
26
|
+
from collections.abc import Callable, Mapping
|
|
26
27
|
from datetime import datetime, timezone
|
|
27
28
|
from pathlib import Path
|
|
28
29
|
|
|
30
|
+
FIX_HINTS: dict[str, str] = {
|
|
31
|
+
"python-missing": "install Python 3.11 or later; on Linux also run: sudo apt install python-is-python3",
|
|
32
|
+
"python-store-alias": "this is the Microsoft Store alias; install Python from python.org or with winget, then turn off the alias in Settings > Apps > App execution aliases",
|
|
33
|
+
"python-old": "install Python 3.11 or later and make it the first python on PATH",
|
|
34
|
+
"git-missing": "install git (Windows: winget install Git.Git; Linux: sudo apt install git)",
|
|
35
|
+
"keys-none": "optional: run caddis keys to add GLM or DeepSeek keys",
|
|
36
|
+
"deny-rules": "run /caddis:setup-project-ai to add the missing deny rules",
|
|
37
|
+
"gitignore": "run /caddis:setup-project-ai to add the missing .caddis ignore entries",
|
|
38
|
+
"misfiled": "move the file into a caddis document folder, or delete it",
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
# Probed contract versions (docs/analysis/*-contract.md). WARN when the installed binary drifts past
|
|
30
42
|
# these — the recorded schemas may be stale and want a re-probe.
|
|
31
43
|
PROBED_VERSIONS = {"codex": "0.137", "agy": "1.1.5"}
|
|
@@ -36,6 +48,29 @@ AGENTS_MD_BUDGET = 200 # always-loaded rules file line budget (mirrors check_do
|
|
|
36
48
|
ARTIFACT_DIRS = (".caddis",)
|
|
37
49
|
ENV_PREFIX = "CADDIS"
|
|
38
50
|
|
|
51
|
+
# Known top-level document and working artifact folders under .caddis/
|
|
52
|
+
KNOWN_CADDIS_DIRS: set[str] = {
|
|
53
|
+
"plans",
|
|
54
|
+
"prd",
|
|
55
|
+
"kb",
|
|
56
|
+
"rca",
|
|
57
|
+
"decisions",
|
|
58
|
+
"parking-lot",
|
|
59
|
+
"reviews",
|
|
60
|
+
"todo-list",
|
|
61
|
+
"comms",
|
|
62
|
+
"handoffs",
|
|
63
|
+
"prompts",
|
|
64
|
+
"relay",
|
|
65
|
+
"session-state",
|
|
66
|
+
"advisory-hub-reports",
|
|
67
|
+
"orchestrator",
|
|
68
|
+
"spawn-session",
|
|
69
|
+
"metrics",
|
|
70
|
+
"agent-docs",
|
|
71
|
+
"backlog",
|
|
72
|
+
}
|
|
73
|
+
|
|
39
74
|
|
|
40
75
|
def _home() -> Path:
|
|
41
76
|
return Path(os.environ.get(f"{ENV_PREFIX}_FAKE_HOME") or Path.home())
|
|
@@ -288,7 +323,7 @@ def _file_signals(dest: Path) -> list[str]:
|
|
|
288
323
|
signals.append(f"{len(dangling)} dangling DOC-MAP link(s) — run /caddis:kb")
|
|
289
324
|
days = days_since_last_doctor()
|
|
290
325
|
if days is not None and days >= 30:
|
|
291
|
-
signals.append(f"{days} days since last caddis doctor — run caddis
|
|
326
|
+
signals.append(f"{days} days since last caddis doctor — run caddis doctor")
|
|
292
327
|
# Listed LAST but it is the most dangerous signal here: the others degrade a session's quality,
|
|
293
328
|
# this one silently runs old code and reports success. It is cheap (a file compare) and almost
|
|
294
329
|
# always empty, so it costs nothing to carry.
|
|
@@ -312,19 +347,30 @@ def nudge_line(dest: Path) -> str | None:
|
|
|
312
347
|
return f"[caddis] {head}{more}"
|
|
313
348
|
|
|
314
349
|
|
|
350
|
+
def _load_sibling(name: str, fallback_dir: Path | str | None = None):
|
|
351
|
+
"""Load a helper module by filename/module name: sibling first, then fallback_dir."""
|
|
352
|
+
scripts = Path(__file__).resolve().parent
|
|
353
|
+
file_name = name if name.endswith(".py") else f"{name}.py"
|
|
354
|
+
mod_name = name[:-3] if name.endswith(".py") else name
|
|
355
|
+
path = scripts / file_name
|
|
356
|
+
if not path.is_file() and fallback_dir:
|
|
357
|
+
path = Path(fallback_dir) / file_name
|
|
358
|
+
if not path.is_file():
|
|
359
|
+
return None
|
|
360
|
+
spec = importlib.util.spec_from_file_location(f"caddis_{mod_name}", path)
|
|
361
|
+
if spec is None or spec.loader is None:
|
|
362
|
+
return None
|
|
363
|
+
module = importlib.util.module_from_spec(spec)
|
|
364
|
+
spec.loader.exec_module(module)
|
|
365
|
+
return module
|
|
366
|
+
|
|
367
|
+
|
|
315
368
|
def hook_errors(dest: Path) -> str:
|
|
316
369
|
"""Recent hook failures, counted by hook. Empty when the ledger is clean."""
|
|
317
370
|
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
371
|
try:
|
|
322
|
-
|
|
323
|
-
if
|
|
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):
|
|
372
|
+
hook_log = _load_sibling("hook_log.py", scripts.parent / "claude-harness" / "scripts")
|
|
373
|
+
if hook_log is None or not hook_log.summarise(str(dest), days=7):
|
|
328
374
|
return ""
|
|
329
375
|
cutoff = time.strftime("%Y-%m-%d", time.localtime(time.time() - 7 * 86400))
|
|
330
376
|
counts: dict[str, int] = {}
|
|
@@ -342,6 +388,182 @@ def hook_errors(dest: Path) -> str:
|
|
|
342
388
|
return "" # a damaged ledger must not break the doctor
|
|
343
389
|
|
|
344
390
|
|
|
391
|
+
def machine_findings(which: Callable[[str], str | None] | None = None,
|
|
392
|
+
version: Callable[[str], str | None] | None = None) -> list[tuple[str, str]]:
|
|
393
|
+
"""Machine faults that stop caddis on a new laptop. Levels: OK, INFO, FAIL (never raises)."""
|
|
394
|
+
which = which or shutil.which # resolved at call time, so a test can patch shutil.which
|
|
395
|
+
version = version or _binary_version
|
|
396
|
+
|
|
397
|
+
out: list[tuple[str, str]] = []
|
|
398
|
+
|
|
399
|
+
# python
|
|
400
|
+
py_path = which("python")
|
|
401
|
+
if py_path is None:
|
|
402
|
+
out.append(("FAIL", f"python (on PATH): not found — {FIX_HINTS['python-missing']}"))
|
|
403
|
+
elif "windowsapps" in py_path.lower() or "WindowsApps" in py_path:
|
|
404
|
+
out.append(("FAIL", f"python (on PATH): [{py_path}] — {FIX_HINTS['python-store-alias']}"))
|
|
405
|
+
else:
|
|
406
|
+
ver_str = None
|
|
407
|
+
try:
|
|
408
|
+
ver_str = version(py_path)
|
|
409
|
+
except Exception:
|
|
410
|
+
ver_str = None
|
|
411
|
+
|
|
412
|
+
m = re.search(r"(\d+)\.(\d+)(?:\.(\d+))?", ver_str or "")
|
|
413
|
+
if not m:
|
|
414
|
+
out.append(("INFO", "python (on PATH): could not read its version"))
|
|
415
|
+
else:
|
|
416
|
+
major, minor = int(m.group(1)), int(m.group(2))
|
|
417
|
+
ver_display = (ver_str or "").strip()
|
|
418
|
+
if ver_display.startswith("Python "):
|
|
419
|
+
ver_display = ver_display[7:].strip()
|
|
420
|
+
if (major, minor) < (3, 11):
|
|
421
|
+
out.append(("FAIL", f"python (on PATH): {ver_display} [{py_path}] — {FIX_HINTS['python-old']}"))
|
|
422
|
+
else:
|
|
423
|
+
out.append(("OK", f"python (on PATH): {ver_display} [{py_path}]"))
|
|
424
|
+
|
|
425
|
+
# git
|
|
426
|
+
git_path = which("git")
|
|
427
|
+
if git_path is None:
|
|
428
|
+
out.append(("FAIL", f"git: not found — {FIX_HINTS['git-missing']}"))
|
|
429
|
+
else:
|
|
430
|
+
out.append(("OK", f"git: [{git_path}]"))
|
|
431
|
+
|
|
432
|
+
return out
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def keys_line(env: Mapping[str, str]) -> tuple[str, str]:
|
|
436
|
+
scripts = Path(__file__).resolve().parent
|
|
437
|
+
try:
|
|
438
|
+
oss_model = _load_sibling("oss_model.py", scripts.parent / "claude-harness" / "scripts")
|
|
439
|
+
if oss_model is None:
|
|
440
|
+
return ("INFO", "keys: could not check")
|
|
441
|
+
|
|
442
|
+
env_dict = dict(env)
|
|
443
|
+
fake_home = env_dict.get(f"{ENV_PREFIX}_FAKE_HOME") or os.environ.get(f"{ENV_PREFIX}_FAKE_HOME")
|
|
444
|
+
if fake_home and "CADDIS_KEYS_FILE" not in env_dict:
|
|
445
|
+
env_dict["CADDIS_KEYS_FILE"] = str(Path(fake_home) / ".caddis" / "keys.env")
|
|
446
|
+
|
|
447
|
+
providers = oss_model.configured_providers(env_dict)
|
|
448
|
+
if providers:
|
|
449
|
+
return ("OK", f"keys: {', '.join(providers)}")
|
|
450
|
+
return ("INFO", f"keys: none — {FIX_HINTS['keys-none']}")
|
|
451
|
+
except Exception:
|
|
452
|
+
return ("INFO", "keys: could not check")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# ── project checks (read-only — WARN or INFO, never FAIL) ─────────────────────
|
|
456
|
+
def deny_rule_findings(dest: Path) -> list[tuple[str, str]]:
|
|
457
|
+
dest = Path(dest)
|
|
458
|
+
if _is_caddis_source(dest):
|
|
459
|
+
return []
|
|
460
|
+
|
|
461
|
+
scripts = Path(__file__).resolve().parent
|
|
462
|
+
tmpl_path = scripts.parent / "settings.template.json"
|
|
463
|
+
if not tmpl_path.is_file():
|
|
464
|
+
tmpl_path = scripts.parent / "claude-harness" / "settings.template.json"
|
|
465
|
+
if not tmpl_path.is_file():
|
|
466
|
+
return []
|
|
467
|
+
|
|
468
|
+
settings_path = dest / ".claude" / "settings.json"
|
|
469
|
+
if not settings_path.is_file():
|
|
470
|
+
return [("INFO", "no .claude/settings.json")]
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
tmpl = json.loads(tmpl_path.read_text(encoding="utf-8"))
|
|
474
|
+
required_deny = tmpl.get("permissions", {}).get("deny", [])
|
|
475
|
+
except Exception:
|
|
476
|
+
return []
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
480
|
+
dest_deny = set(settings.get("permissions", {}).get("deny", []))
|
|
481
|
+
except Exception:
|
|
482
|
+
dest_deny = set()
|
|
483
|
+
|
|
484
|
+
missing = [r for r in required_deny if r not in dest_deny]
|
|
485
|
+
if missing:
|
|
486
|
+
return [("WARN", f"deny rules: {len(missing)} missing — {FIX_HINTS['deny-rules']}")]
|
|
487
|
+
return []
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def gitignore_findings(dest: Path) -> list[tuple[str, str]]:
|
|
491
|
+
dest = Path(dest)
|
|
492
|
+
if _is_caddis_source(dest):
|
|
493
|
+
return []
|
|
494
|
+
if not (dest / ".caddis").is_dir():
|
|
495
|
+
return []
|
|
496
|
+
|
|
497
|
+
scripts = Path(__file__).resolve().parent
|
|
498
|
+
try:
|
|
499
|
+
setup_mod = _load_sibling("setup_project_ai.py", scripts.parent / "scripts")
|
|
500
|
+
if setup_mod is None:
|
|
501
|
+
return [("INFO", "gitignore: could not check")]
|
|
502
|
+
artifact_gitignore = getattr(setup_mod, "ARTIFACT_GITIGNORE", None)
|
|
503
|
+
if not artifact_gitignore:
|
|
504
|
+
return [("INFO", "gitignore: could not check")]
|
|
505
|
+
|
|
506
|
+
entries = [
|
|
507
|
+
line.strip()
|
|
508
|
+
for line in artifact_gitignore.splitlines()
|
|
509
|
+
if line.strip() and not line.strip().startswith("#")
|
|
510
|
+
]
|
|
511
|
+
|
|
512
|
+
caddis_ignore = dest / ".caddis" / ".gitignore"
|
|
513
|
+
caddis_lines: set[str] = set()
|
|
514
|
+
if caddis_ignore.is_file():
|
|
515
|
+
try:
|
|
516
|
+
caddis_lines = {line.strip() for line in caddis_ignore.read_text(encoding="utf-8", errors="replace").splitlines()}
|
|
517
|
+
except OSError:
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
root_ignore = dest / ".gitignore"
|
|
521
|
+
root_lines: set[str] = set()
|
|
522
|
+
if root_ignore.is_file():
|
|
523
|
+
try:
|
|
524
|
+
root_lines = {line.strip() for line in root_ignore.read_text(encoding="utf-8", errors="replace").splitlines()}
|
|
525
|
+
except OSError:
|
|
526
|
+
pass
|
|
527
|
+
|
|
528
|
+
missing: list[str] = []
|
|
529
|
+
for entry in entries:
|
|
530
|
+
if entry in caddis_lines or f".caddis/{entry}" in root_lines:
|
|
531
|
+
continue
|
|
532
|
+
missing.append(entry)
|
|
533
|
+
|
|
534
|
+
if missing:
|
|
535
|
+
first_three = ", ".join(missing[:3])
|
|
536
|
+
return [("WARN", f"gitignore: {len(missing)} missing ({first_three}) — {FIX_HINTS['gitignore']}")]
|
|
537
|
+
return []
|
|
538
|
+
except Exception:
|
|
539
|
+
return [("INFO", "gitignore: could not check")]
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def misfiled_findings(dest: Path) -> list[tuple[str, str]]:
|
|
543
|
+
dest = Path(dest)
|
|
544
|
+
if _is_caddis_source(dest):
|
|
545
|
+
return []
|
|
546
|
+
|
|
547
|
+
caddis_dir = dest / ".caddis"
|
|
548
|
+
if not caddis_dir.is_dir():
|
|
549
|
+
return []
|
|
550
|
+
|
|
551
|
+
out: list[tuple[str, str]] = []
|
|
552
|
+
try:
|
|
553
|
+
subdirs = sorted(p for p in caddis_dir.iterdir() if p.is_dir())
|
|
554
|
+
except OSError:
|
|
555
|
+
return []
|
|
556
|
+
|
|
557
|
+
for top_dir in subdirs:
|
|
558
|
+
if top_dir.name in _SKIP_DIRS:
|
|
559
|
+
continue
|
|
560
|
+
if top_dir.name not in KNOWN_CADDIS_DIRS:
|
|
561
|
+
md_files = list(top_dir.rglob("*.md"))
|
|
562
|
+
if md_files:
|
|
563
|
+
out.append(("WARN", f".caddis/{top_dir.name}/ is not a caddis folder ({len(md_files)} file(s)) — {FIX_HINTS['misfiled']}"))
|
|
564
|
+
return out
|
|
565
|
+
|
|
566
|
+
|
|
345
567
|
# ── report ───────────────────────────────────────────────────────────────────
|
|
346
568
|
def run(dest: Path, quiet: bool) -> int:
|
|
347
569
|
if quiet: # SessionStart mode: only the nudge, never non-zero
|
|
@@ -352,7 +574,7 @@ def run(dest: Path, quiet: bool) -> int:
|
|
|
352
574
|
|
|
353
575
|
hard = 0
|
|
354
576
|
print(f"=== caddis doctor — {dest} ===")
|
|
355
|
-
print(f"python: {sys.version.split()[0]} ({sys.executable})")
|
|
577
|
+
print(f"python (running this check): {sys.version.split()[0]} ({sys.executable})")
|
|
356
578
|
for name in ("codex", "agy", "claude"):
|
|
357
579
|
path = _which(name)
|
|
358
580
|
if path:
|
|
@@ -361,6 +583,26 @@ def run(dest: Path, quiet: bool) -> int:
|
|
|
361
583
|
else:
|
|
362
584
|
print(f" {name:7} not found on PATH (install, or open a new terminal so PATH refreshes)")
|
|
363
585
|
|
|
586
|
+
print("-- machine")
|
|
587
|
+
for level, text in machine_findings():
|
|
588
|
+
if level == "FAIL":
|
|
589
|
+
hard = 1
|
|
590
|
+
print(f" {level:4} {text}")
|
|
591
|
+
k_level, k_text = keys_line(os.environ)
|
|
592
|
+
if k_level == "FAIL":
|
|
593
|
+
hard = 1
|
|
594
|
+
print(f" {k_level:4} {k_text}")
|
|
595
|
+
|
|
596
|
+
print("-- project")
|
|
597
|
+
proj_findings = deny_rule_findings(dest) + gitignore_findings(dest) + misfiled_findings(dest)
|
|
598
|
+
if proj_findings:
|
|
599
|
+
for level, text in proj_findings:
|
|
600
|
+
if level == "FAIL":
|
|
601
|
+
hard = 1
|
|
602
|
+
print(f" {level:4} {text}")
|
|
603
|
+
else:
|
|
604
|
+
print(" OK deny rules, gitignore entries and document folders clean")
|
|
605
|
+
|
|
364
606
|
print("-- rules integrity")
|
|
365
607
|
findings = rules_findings(dest)
|
|
366
608
|
if findings:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Base harness settings deployed by setup-project-ai: permissions + a status line. The generator adds stack-specific Bash allow entries (pytest/npm/etc.) based on detection. HOOKS ARE NOT DEFINED HERE: the harness loop hooks (relay.md resume on SessionStart/PreCompact, end-of-session nudge + usage digest on Stop, auto-lint on Edit/Write) ship with the caddis plugin via plugin/hooks/hooks.json and auto-activate on install — defining them here too would double-fire and re-introduce machine-specific paths. The default loop is just a plan file + relay.md; there is no opt-in pipeline/MCP power-mode.",
|
|
3
|
+
"$comment_permissions": "IMPORTANT: any entry in `ask` overrides bypassPermissions (yolo) mode and forces a prompt every time, even when the user has opted into no-prompts. Keep `ask` empty (or as minimal as possible) so bypassPermissions actually bypasses. `Bash(git push:*)` was moved out of `ask` into `allow` for exactly this reason — it was forcing a push prompt on every /ship under bypassPermissions.",
|
|
4
|
+
"permissions": {
|
|
5
|
+
"allow": [
|
|
6
|
+
"Read",
|
|
7
|
+
"Edit",
|
|
8
|
+
"Write",
|
|
9
|
+
"Glob",
|
|
10
|
+
"Grep",
|
|
11
|
+
"Bash(git status)",
|
|
12
|
+
"Bash(git diff:*)",
|
|
13
|
+
"Bash(git add:*)",
|
|
14
|
+
"Bash(git commit:*)",
|
|
15
|
+
"Bash(git log:*)",
|
|
16
|
+
"Bash(git push:*)"
|
|
17
|
+
],
|
|
18
|
+
"ask": [],
|
|
19
|
+
"deny": [
|
|
20
|
+
"Read(**/.env)",
|
|
21
|
+
"Read(**/.env.local)",
|
|
22
|
+
"Read(**/.env.*.local)",
|
|
23
|
+
"Read(**/.env.production)",
|
|
24
|
+
"Read(**/.env.development)",
|
|
25
|
+
"Edit(**/.env)",
|
|
26
|
+
"Edit(**/.env.local)",
|
|
27
|
+
"Edit(**/.env.*.local)",
|
|
28
|
+
"Edit(**/.env.production)",
|
|
29
|
+
"Edit(**/.env.development)",
|
|
30
|
+
"Write(**/.env)",
|
|
31
|
+
"Write(**/.env.local)",
|
|
32
|
+
"Write(**/.env.*.local)",
|
|
33
|
+
"Write(**/.env.production)",
|
|
34
|
+
"Write(**/.env.development)"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
package/bundles/manifest.json
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1209,7 +1209,7 @@ var require_command = __commonJS({
|
|
|
1209
1209
|
"use strict";
|
|
1210
1210
|
var EventEmitter = __require("events").EventEmitter;
|
|
1211
1211
|
var childProcess = __require("child_process");
|
|
1212
|
-
var
|
|
1212
|
+
var path6 = __require("path");
|
|
1213
1213
|
var fs = __require("fs");
|
|
1214
1214
|
var process3 = __require("process");
|
|
1215
1215
|
var { Argument: Argument2, humanReadableArgName } = require_argument();
|
|
@@ -2222,9 +2222,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2222
2222
|
let launchWithNode = false;
|
|
2223
2223
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
2224
2224
|
function findFile(baseDir, baseName) {
|
|
2225
|
-
const localBin =
|
|
2225
|
+
const localBin = path6.resolve(baseDir, baseName);
|
|
2226
2226
|
if (fs.existsSync(localBin)) return localBin;
|
|
2227
|
-
if (sourceExt.includes(
|
|
2227
|
+
if (sourceExt.includes(path6.extname(baseName))) return void 0;
|
|
2228
2228
|
const foundExt = sourceExt.find(
|
|
2229
2229
|
(ext) => fs.existsSync(`${localBin}${ext}`)
|
|
2230
2230
|
);
|
|
@@ -2242,17 +2242,17 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2242
2242
|
} catch {
|
|
2243
2243
|
resolvedScriptPath = this._scriptPath;
|
|
2244
2244
|
}
|
|
2245
|
-
executableDir =
|
|
2246
|
-
|
|
2245
|
+
executableDir = path6.resolve(
|
|
2246
|
+
path6.dirname(resolvedScriptPath),
|
|
2247
2247
|
executableDir
|
|
2248
2248
|
);
|
|
2249
2249
|
}
|
|
2250
2250
|
if (executableDir) {
|
|
2251
2251
|
let localFile = findFile(executableDir, executableFile);
|
|
2252
2252
|
if (!localFile && !subcommand._executableFile && this._scriptPath) {
|
|
2253
|
-
const legacyName =
|
|
2253
|
+
const legacyName = path6.basename(
|
|
2254
2254
|
this._scriptPath,
|
|
2255
|
-
|
|
2255
|
+
path6.extname(this._scriptPath)
|
|
2256
2256
|
);
|
|
2257
2257
|
if (legacyName !== this._name) {
|
|
2258
2258
|
localFile = findFile(
|
|
@@ -2263,7 +2263,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2263
2263
|
}
|
|
2264
2264
|
executableFile = localFile || executableFile;
|
|
2265
2265
|
}
|
|
2266
|
-
launchWithNode = sourceExt.includes(
|
|
2266
|
+
launchWithNode = sourceExt.includes(path6.extname(executableFile));
|
|
2267
2267
|
let proc;
|
|
2268
2268
|
if (process3.platform !== "win32") {
|
|
2269
2269
|
if (launchWithNode) {
|
|
@@ -3178,7 +3178,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3178
3178
|
* @return {Command}
|
|
3179
3179
|
*/
|
|
3180
3180
|
nameFromFilename(filename) {
|
|
3181
|
-
this._name =
|
|
3181
|
+
this._name = path6.basename(filename, path6.extname(filename));
|
|
3182
3182
|
return this;
|
|
3183
3183
|
}
|
|
3184
3184
|
/**
|
|
@@ -3192,9 +3192,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3192
3192
|
* @param {string} [path]
|
|
3193
3193
|
* @return {(string|null|Command)}
|
|
3194
3194
|
*/
|
|
3195
|
-
executableDir(
|
|
3196
|
-
if (
|
|
3197
|
-
this._executableDir =
|
|
3195
|
+
executableDir(path7) {
|
|
3196
|
+
if (path7 === void 0) return this._executableDir;
|
|
3197
|
+
this._executableDir = path7;
|
|
3198
3198
|
return this;
|
|
3199
3199
|
}
|
|
3200
3200
|
/**
|
|
@@ -3731,6 +3731,23 @@ var require_lib = __commonJS({
|
|
|
3731
3731
|
}
|
|
3732
3732
|
});
|
|
3733
3733
|
|
|
3734
|
+
// src/util/which.ts
|
|
3735
|
+
async function findBin(name) {
|
|
3736
|
+
try {
|
|
3737
|
+
const found = await (0, import_which.default)(name, { nothrow: true });
|
|
3738
|
+
return found ?? null;
|
|
3739
|
+
} catch {
|
|
3740
|
+
return null;
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
var import_which;
|
|
3744
|
+
var init_which = __esm({
|
|
3745
|
+
"src/util/which.ts"() {
|
|
3746
|
+
"use strict";
|
|
3747
|
+
import_which = __toESM(require_lib(), 1);
|
|
3748
|
+
}
|
|
3749
|
+
});
|
|
3750
|
+
|
|
3734
3751
|
// src/util/pkg.ts
|
|
3735
3752
|
import { readFileSync, existsSync } from "fs";
|
|
3736
3753
|
import { fileURLToPath } from "url";
|
|
@@ -4843,7 +4860,7 @@ var init_init = __esm({
|
|
|
4843
4860
|
});
|
|
4844
4861
|
|
|
4845
4862
|
// src/providers/resolve.ts
|
|
4846
|
-
import { readFileSync as
|
|
4863
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
4847
4864
|
import { homedir } from "os";
|
|
4848
4865
|
import { isAbsolute, join } from "path";
|
|
4849
4866
|
function expandHome(p2) {
|
|
@@ -4856,10 +4873,10 @@ function keysFilePath(env) {
|
|
|
4856
4873
|
const expanded = expandHome(configured);
|
|
4857
4874
|
return isAbsolute(expanded) ? expanded : expanded;
|
|
4858
4875
|
}
|
|
4859
|
-
function parseKeysFile(
|
|
4876
|
+
function parseKeysFile(path6) {
|
|
4860
4877
|
let text;
|
|
4861
4878
|
try {
|
|
4862
|
-
text =
|
|
4879
|
+
text = readFileSync3(path6, "utf8");
|
|
4863
4880
|
} catch {
|
|
4864
4881
|
return {};
|
|
4865
4882
|
}
|
|
@@ -4917,7 +4934,7 @@ __export(keys_exports, {
|
|
|
4917
4934
|
probeKey: () => probeKey,
|
|
4918
4935
|
upsertKeyLines: () => upsertKeyLines
|
|
4919
4936
|
});
|
|
4920
|
-
import { chmodSync, mkdirSync
|
|
4937
|
+
import { chmodSync, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
4921
4938
|
import { dirname } from "path";
|
|
4922
4939
|
async function probeKey(baseUrl, model, apiKey) {
|
|
4923
4940
|
if (!baseUrl) return { ok: false, rejected: false, detail: "no endpoint for this provider" };
|
|
@@ -4971,18 +4988,18 @@ function upsertKeyLines(existing, updates) {
|
|
|
4971
4988
|
if (!text.endsWith("\n")) text += "\n";
|
|
4972
4989
|
return text;
|
|
4973
4990
|
}
|
|
4974
|
-
function restrictPermissions(
|
|
4991
|
+
function restrictPermissions(path6) {
|
|
4975
4992
|
if (process.platform === "win32") return null;
|
|
4976
4993
|
try {
|
|
4977
|
-
chmodSync(
|
|
4994
|
+
chmodSync(path6, 384);
|
|
4978
4995
|
return null;
|
|
4979
4996
|
} catch (error) {
|
|
4980
4997
|
return error instanceof Error ? error.message : String(error);
|
|
4981
4998
|
}
|
|
4982
4999
|
}
|
|
4983
5000
|
async function keys(options = {}) {
|
|
4984
|
-
const
|
|
4985
|
-
const fileKeys = parseKeysFile(
|
|
5001
|
+
const path6 = keysFilePath(process.env);
|
|
5002
|
+
const fileKeys = parseKeysFile(path6);
|
|
4986
5003
|
const names = Object.keys(PROVIDERS).sort();
|
|
4987
5004
|
const current = names.map((name) => {
|
|
4988
5005
|
const preset = PROVIDERS[name];
|
|
@@ -4998,7 +5015,7 @@ async function keys(options = {}) {
|
|
|
4998
5015
|
if (options.check) {
|
|
4999
5016
|
const configured = current.filter((entry) => entry.value);
|
|
5000
5017
|
if (!configured.length) {
|
|
5001
|
-
process.stdout.write(`no provider keys configured (looked in $ENV and ${
|
|
5018
|
+
process.stdout.write(`no provider keys configured (looked in $ENV and ${path6})
|
|
5002
5019
|
`);
|
|
5003
5020
|
return 1;
|
|
5004
5021
|
}
|
|
@@ -5013,7 +5030,7 @@ async function keys(options = {}) {
|
|
|
5013
5030
|
return bad ? 1 : 0;
|
|
5014
5031
|
}
|
|
5015
5032
|
Ie("caddis keys");
|
|
5016
|
-
process.stdout.write(` keys file: ${
|
|
5033
|
+
process.stdout.write(` keys file: ${path6}
|
|
5017
5034
|
|
|
5018
5035
|
`);
|
|
5019
5036
|
for (const entry of current) {
|
|
@@ -5073,20 +5090,20 @@ async function keys(options = {}) {
|
|
|
5073
5090
|
return 0;
|
|
5074
5091
|
}
|
|
5075
5092
|
if (options.dryRun) {
|
|
5076
|
-
Se(`dry run \u2014 would write ${Object.keys(collected).join(", ")} to ${
|
|
5093
|
+
Se(`dry run \u2014 would write ${Object.keys(collected).join(", ")} to ${path6}`);
|
|
5077
5094
|
return 0;
|
|
5078
5095
|
}
|
|
5079
5096
|
let existing = "";
|
|
5080
5097
|
try {
|
|
5081
|
-
existing =
|
|
5098
|
+
existing = readFileSync4(path6, "utf8");
|
|
5082
5099
|
} catch {
|
|
5083
5100
|
existing = "";
|
|
5084
5101
|
}
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
const permWarning = restrictPermissions(
|
|
5102
|
+
mkdirSync(dirname(path6), { recursive: true });
|
|
5103
|
+
writeFileSync(path6, upsertKeyLines(existing, collected), { encoding: "utf8" });
|
|
5104
|
+
const permWarning = restrictPermissions(path6);
|
|
5088
5105
|
process.stdout.write(`
|
|
5089
|
-
wrote ${Object.keys(collected).join(", ")} to ${
|
|
5106
|
+
wrote ${Object.keys(collected).join(", ")} to ${path6}
|
|
5090
5107
|
`);
|
|
5091
5108
|
if (permWarning) process.stdout.write(color.yellow(` could not set 0600 on the keys file: ${permWarning}
|
|
5092
5109
|
`));
|
|
@@ -5160,18 +5177,22 @@ var doctor_exports = {};
|
|
|
5160
5177
|
__export(doctor_exports, {
|
|
5161
5178
|
doctor: () => doctor
|
|
5162
5179
|
});
|
|
5180
|
+
import { existsSync as existsSync5 } from "fs";
|
|
5163
5181
|
import os3 from "os";
|
|
5164
5182
|
import process2 from "process";
|
|
5183
|
+
import path5 from "path";
|
|
5165
5184
|
async function doctor(options) {
|
|
5166
5185
|
const report = await gather(options.adapters, { checkCliUpdate: true });
|
|
5167
5186
|
const findings = [];
|
|
5168
5187
|
if (options.json) {
|
|
5169
|
-
|
|
5188
|
+
const project = await renderProject(findings, { quiet: true });
|
|
5189
|
+
line(JSON.stringify(toJson(report, project), null, 2));
|
|
5170
5190
|
return 0;
|
|
5171
5191
|
}
|
|
5172
5192
|
renderBanner({ cliVersion: report.cliVersion, poolVersion: report.poolVersion });
|
|
5173
5193
|
renderEnvironment(report, findings);
|
|
5174
5194
|
renderAgents(report, findings);
|
|
5195
|
+
await renderProject(findings);
|
|
5175
5196
|
renderSummary(report, findings);
|
|
5176
5197
|
return options.strict && findings.some((f) => f.kind === "problem") ? 1 : 0;
|
|
5177
5198
|
}
|
|
@@ -5236,6 +5257,57 @@ function renderAgents(report, problems) {
|
|
|
5236
5257
|
}
|
|
5237
5258
|
}
|
|
5238
5259
|
}
|
|
5260
|
+
async function renderProject(problems, options) {
|
|
5261
|
+
const quiet = options?.quiet ?? false;
|
|
5262
|
+
const python = await findBin("python");
|
|
5263
|
+
if (!python) {
|
|
5264
|
+
if (!quiet) {
|
|
5265
|
+
heading("Project");
|
|
5266
|
+
item("fail", "python not on PATH");
|
|
5267
|
+
}
|
|
5268
|
+
problems.push({
|
|
5269
|
+
kind: "problem",
|
|
5270
|
+
text: "python not on PATH",
|
|
5271
|
+
fix: "install Python 3.11 or later (see docs/ONBOARDING.md)"
|
|
5272
|
+
});
|
|
5273
|
+
return { ran: false, exitCode: null };
|
|
5274
|
+
}
|
|
5275
|
+
const bundle = bundlePath("antigravity-plugin", { requireManifest: false });
|
|
5276
|
+
const script = bundle ? path5.join(bundle, "scripts", "claudster_doctor.py") : null;
|
|
5277
|
+
if (!script || !existsSync5(script)) {
|
|
5278
|
+
if (!quiet) {
|
|
5279
|
+
heading("Project");
|
|
5280
|
+
item("fail", "claudster_doctor.py missing");
|
|
5281
|
+
}
|
|
5282
|
+
problems.push({
|
|
5283
|
+
kind: "problem",
|
|
5284
|
+
text: "claudster_doctor.py missing from antigravity-plugin bundle",
|
|
5285
|
+
fix: "reinstall: npm i -g @caddis/cli"
|
|
5286
|
+
});
|
|
5287
|
+
return { ran: false, exitCode: null };
|
|
5288
|
+
}
|
|
5289
|
+
if (!quiet) {
|
|
5290
|
+
heading("Project");
|
|
5291
|
+
}
|
|
5292
|
+
const result = await run(python, [script, "--dest", process2.cwd()], { timeout: 6e4 });
|
|
5293
|
+
if (!quiet && result.stdout) {
|
|
5294
|
+
const lines = result.stdout.replace(/\r\n/g, "\n").split("\n");
|
|
5295
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
5296
|
+
lines.pop();
|
|
5297
|
+
}
|
|
5298
|
+
for (const l2 of lines) {
|
|
5299
|
+
line(l2 ? ` ${l2}` : "");
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
if (result.code === 1 || !result.ok) {
|
|
5303
|
+
problems.push({
|
|
5304
|
+
kind: "problem",
|
|
5305
|
+
text: "project health check failed",
|
|
5306
|
+
fix: "read the FAIL lines above"
|
|
5307
|
+
});
|
|
5308
|
+
}
|
|
5309
|
+
return { ran: true, exitCode: result.code };
|
|
5310
|
+
}
|
|
5239
5311
|
function renderSummary(report, findings) {
|
|
5240
5312
|
heading("Summary");
|
|
5241
5313
|
const counts = report.agents.reduce(
|
|
@@ -5368,7 +5440,7 @@ function problemFor(entry, poolVersion) {
|
|
|
5368
5440
|
return null;
|
|
5369
5441
|
}
|
|
5370
5442
|
}
|
|
5371
|
-
function toJson(report) {
|
|
5443
|
+
function toJson(report, project) {
|
|
5372
5444
|
return {
|
|
5373
5445
|
cliVersion: report.cliVersion,
|
|
5374
5446
|
cliUpdate: report.cliUpdate ?? null,
|
|
@@ -5389,7 +5461,8 @@ function toJson(report) {
|
|
|
5389
5461
|
extrasVersion: entry.status.extras?.version ?? null,
|
|
5390
5462
|
extrasDrift: entry.extrasDrift ?? null,
|
|
5391
5463
|
note: entry.status.note ?? entry.detection.note ?? null
|
|
5392
|
-
}))
|
|
5464
|
+
})),
|
|
5465
|
+
project: project ?? { ran: false, exitCode: null }
|
|
5393
5466
|
};
|
|
5394
5467
|
}
|
|
5395
5468
|
var REQUIRED_NODE_MAJOR, REQUIRED_NODE_MINOR;
|
|
@@ -5397,7 +5470,9 @@ var init_doctor = __esm({
|
|
|
5397
5470
|
"src/commands/doctor.ts"() {
|
|
5398
5471
|
"use strict";
|
|
5399
5472
|
init_log();
|
|
5473
|
+
init_exec();
|
|
5400
5474
|
init_pkg();
|
|
5475
|
+
init_which();
|
|
5401
5476
|
init_report();
|
|
5402
5477
|
REQUIRED_NODE_MAJOR = 20;
|
|
5403
5478
|
REQUIRED_NODE_MINOR = 19;
|
|
@@ -5544,19 +5619,7 @@ var {
|
|
|
5544
5619
|
|
|
5545
5620
|
// src/agents/claude.ts
|
|
5546
5621
|
init_exec();
|
|
5547
|
-
|
|
5548
|
-
// src/util/which.ts
|
|
5549
|
-
var import_which = __toESM(require_lib(), 1);
|
|
5550
|
-
async function findBin(name) {
|
|
5551
|
-
try {
|
|
5552
|
-
const found = await (0, import_which.default)(name, { nothrow: true });
|
|
5553
|
-
return found ?? null;
|
|
5554
|
-
} catch {
|
|
5555
|
-
return null;
|
|
5556
|
-
}
|
|
5557
|
-
}
|
|
5558
|
-
|
|
5559
|
-
// src/agents/claude.ts
|
|
5622
|
+
init_which();
|
|
5560
5623
|
var BIN = "claude";
|
|
5561
5624
|
var MARKETPLACE = "caddis";
|
|
5562
5625
|
var PLUGIN = "caddis@caddis";
|
|
@@ -5592,14 +5655,14 @@ function* pluginBlocks(stdout) {
|
|
|
5592
5655
|
if (current) yield current;
|
|
5593
5656
|
}
|
|
5594
5657
|
async function detect() {
|
|
5595
|
-
const
|
|
5596
|
-
if (!
|
|
5658
|
+
const path6 = await findBin(BIN);
|
|
5659
|
+
if (!path6) {
|
|
5597
5660
|
return { present: false, note: "no `claude` binary on PATH" };
|
|
5598
5661
|
}
|
|
5599
5662
|
const version = await run(BIN, ["--version"], { timeout: 2e4 });
|
|
5600
5663
|
return {
|
|
5601
5664
|
present: true,
|
|
5602
|
-
path:
|
|
5665
|
+
path: path6,
|
|
5603
5666
|
agentVersion: version.ok ? version.stdout.trim().split(/\s+/)[0] || void 0 : void 0,
|
|
5604
5667
|
note: version.ok ? void 0 : "binary found but `claude --version` failed"
|
|
5605
5668
|
};
|
|
@@ -5630,6 +5693,7 @@ async function status() {
|
|
|
5630
5693
|
function steps(action) {
|
|
5631
5694
|
if (action === "install") {
|
|
5632
5695
|
return [
|
|
5696
|
+
{ cmd: BIN, args: ["plugin", "marketplace", "add", "saajunaid/caddis-plugin"] },
|
|
5633
5697
|
{ cmd: BIN, args: ["plugin", "marketplace", "update", MARKETPLACE] },
|
|
5634
5698
|
{ cmd: BIN, args: ["plugin", "install", PLUGIN] }
|
|
5635
5699
|
];
|
|
@@ -5657,13 +5721,25 @@ async function drive(action, options) {
|
|
|
5657
5721
|
for (const step of planned) {
|
|
5658
5722
|
const command = formatCommand(step.cmd, step.args);
|
|
5659
5723
|
const result = await run(step.cmd, step.args);
|
|
5724
|
+
const isMarketplaceAdd = step.args[1] === "marketplace" && step.args[2] === "add";
|
|
5725
|
+
const isMarketplaceUpdate = step.args[1] === "marketplace" && step.args[2] === "update";
|
|
5726
|
+
let ok = result.ok;
|
|
5727
|
+
let code = result.code;
|
|
5728
|
+
if (!ok && isMarketplaceAdd) {
|
|
5729
|
+
const output = `${result.stdout}
|
|
5730
|
+
${result.stderr}`;
|
|
5731
|
+
if (/already (exists|added|configured|installed|on disk)/i.test(output)) {
|
|
5732
|
+
ok = true;
|
|
5733
|
+
code = 0;
|
|
5734
|
+
}
|
|
5735
|
+
}
|
|
5660
5736
|
results.push({
|
|
5661
5737
|
command,
|
|
5662
|
-
ok
|
|
5663
|
-
code
|
|
5664
|
-
output:
|
|
5738
|
+
ok,
|
|
5739
|
+
code,
|
|
5740
|
+
output: ok ? void 0 : tail(result.stderr || result.stdout || result.failure || "")
|
|
5665
5741
|
});
|
|
5666
|
-
if (!
|
|
5742
|
+
if (!ok && !isMarketplaceUpdate) {
|
|
5667
5743
|
return { ok: false, skipped: false, steps: results, message: `\`${command}\` failed` };
|
|
5668
5744
|
}
|
|
5669
5745
|
}
|
|
@@ -5684,10 +5760,11 @@ var claudeAdapter = {
|
|
|
5684
5760
|
|
|
5685
5761
|
// src/agents/agy.ts
|
|
5686
5762
|
init_exec();
|
|
5763
|
+
init_which();
|
|
5764
|
+
init_pkg();
|
|
5687
5765
|
import { readFileSync as readFileSync2, existsSync as existsSync2, mkdtempSync, cpSync, rmSync } from "fs";
|
|
5688
5766
|
import os from "os";
|
|
5689
5767
|
import path2 from "path";
|
|
5690
|
-
init_pkg();
|
|
5691
5768
|
var BIN2 = "agy";
|
|
5692
5769
|
var BUNDLE = "antigravity-plugin";
|
|
5693
5770
|
var PLUGIN2 = "caddis";
|
|
@@ -5815,24 +5892,24 @@ async function drive2(_action, options) {
|
|
|
5815
5892
|
}
|
|
5816
5893
|
const cleanups = [];
|
|
5817
5894
|
try {
|
|
5818
|
-
const
|
|
5895
|
+
const steps3 = [];
|
|
5819
5896
|
for (const step of planned) {
|
|
5820
5897
|
const command = formatCommand(step.cmd, step.args);
|
|
5821
5898
|
const staged = stageForAgy(step.bundleDir);
|
|
5822
5899
|
cleanups.push(staged.cleanup);
|
|
5823
5900
|
const execArgs = [...step.args.slice(0, -1), staged.installPath];
|
|
5824
5901
|
const result = await run(step.cmd, execArgs);
|
|
5825
|
-
|
|
5902
|
+
steps3.push({
|
|
5826
5903
|
command,
|
|
5827
5904
|
ok: result.ok,
|
|
5828
5905
|
code: result.code,
|
|
5829
5906
|
output: result.ok ? void 0 : (result.stderr || result.stdout || result.failure || "").trim().split(/\r?\n/).slice(-3).join("\n")
|
|
5830
5907
|
});
|
|
5831
5908
|
if (!result.ok) {
|
|
5832
|
-
return { ok: false, skipped: false, steps:
|
|
5909
|
+
return { ok: false, skipped: false, steps: steps3, message: `\`${command}\` failed` };
|
|
5833
5910
|
}
|
|
5834
5911
|
}
|
|
5835
|
-
return { ok: true, skipped: false, steps:
|
|
5912
|
+
return { ok: true, skipped: false, steps: steps3 };
|
|
5836
5913
|
} finally {
|
|
5837
5914
|
cleanups.forEach((fn) => fn());
|
|
5838
5915
|
}
|
|
@@ -5849,12 +5926,14 @@ var agyAdapter = {
|
|
|
5849
5926
|
|
|
5850
5927
|
// src/agents/codex.ts
|
|
5851
5928
|
init_exec();
|
|
5852
|
-
|
|
5929
|
+
init_which();
|
|
5930
|
+
import { existsSync as existsSync3, rmSync as rmSync2 } from "fs";
|
|
5853
5931
|
import os2 from "os";
|
|
5854
5932
|
import path3 from "path";
|
|
5855
|
-
init_pkg();
|
|
5856
5933
|
var BIN3 = "codex";
|
|
5857
|
-
var
|
|
5934
|
+
var MARKETPLACE_URL = "https://github.com/saajunaid/caddis-plugin";
|
|
5935
|
+
var MARKETPLACE2 = "caddis";
|
|
5936
|
+
var PLUGIN3 = "caddis-codex@caddis";
|
|
5858
5937
|
function caddisSkillsDir(home = os2.homedir()) {
|
|
5859
5938
|
return path3.join(home, ".codex", "skills", "caddis");
|
|
5860
5939
|
}
|
|
@@ -5874,7 +5953,6 @@ async function detect3() {
|
|
|
5874
5953
|
return {
|
|
5875
5954
|
present: true,
|
|
5876
5955
|
path: binPath,
|
|
5877
|
-
// `codex --version` prints "codex-cli 0.150.1"; take the trailing token.
|
|
5878
5956
|
agentVersion: version.ok ? version.stdout.trim().split(/\s+/).pop() || void 0 : void 0,
|
|
5879
5957
|
note: version.ok ? void 0 : "binary found but `codex --version` failed"
|
|
5880
5958
|
};
|
|
@@ -5883,104 +5961,146 @@ function shortenHome2(target) {
|
|
|
5883
5961
|
const home = os2.homedir();
|
|
5884
5962
|
return target.startsWith(home) ? path3.join("~", target.slice(home.length)) : target;
|
|
5885
5963
|
}
|
|
5886
|
-
function
|
|
5887
|
-
const
|
|
5888
|
-
const
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5964
|
+
function parseCodexPluginList(stdout, plugin = PLUGIN3) {
|
|
5965
|
+
const wanted = plugin.toLowerCase();
|
|
5966
|
+
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
5967
|
+
const line2 = rawLine.trim();
|
|
5968
|
+
if (!line2.toLowerCase().startsWith(wanted)) continue;
|
|
5969
|
+
const rest = line2.slice(wanted.length).trim();
|
|
5970
|
+
if (/^not installed/i.test(rest)) {
|
|
5971
|
+
return { installed: false };
|
|
5972
|
+
}
|
|
5973
|
+
const match = rest.match(/^installed,\s*(enabled|disabled)(?:\s+([^\s]+))?/i);
|
|
5974
|
+
if (match) {
|
|
5975
|
+
return {
|
|
5976
|
+
installed: true,
|
|
5977
|
+
disabled: match[1]?.toLowerCase() === "disabled",
|
|
5978
|
+
version: match[2]
|
|
5979
|
+
};
|
|
5980
|
+
}
|
|
5981
|
+
const simpleMatch = rest.match(/^installed(?:\s+([^\s]+))?/i);
|
|
5982
|
+
if (simpleMatch) {
|
|
5983
|
+
return {
|
|
5984
|
+
installed: true,
|
|
5985
|
+
disabled: false,
|
|
5986
|
+
version: simpleMatch[1]
|
|
5987
|
+
};
|
|
5894
5988
|
}
|
|
5895
|
-
return { installed: true, source: shortenHome2(marker), note: "version marker present but unreadable" };
|
|
5896
|
-
}
|
|
5897
|
-
if (existsSync3(dir)) {
|
|
5898
|
-
return {
|
|
5899
|
-
installed: true,
|
|
5900
|
-
source: shortenHome2(dir),
|
|
5901
|
-
note: "skills present but no version marker \u2014 installed by hand, or by a caddis older than this one"
|
|
5902
|
-
};
|
|
5903
5989
|
}
|
|
5904
|
-
return
|
|
5990
|
+
return null;
|
|
5905
5991
|
}
|
|
5906
5992
|
async function status3() {
|
|
5907
5993
|
const detected = await detect3();
|
|
5908
5994
|
if (!detected.present) return { installed: false, note: "agent not installed" };
|
|
5909
|
-
|
|
5995
|
+
const listed = await run(BIN3, ["plugin", "list"], { timeout: 6e4 });
|
|
5996
|
+
if (!listed.ok) {
|
|
5997
|
+
return {
|
|
5998
|
+
installed: false,
|
|
5999
|
+
source: "`codex plugin list`",
|
|
6000
|
+
note: `could not read plugin list (${listed.failure ?? `exit ${listed.code}`})`
|
|
6001
|
+
};
|
|
6002
|
+
}
|
|
6003
|
+
const entry = parseCodexPluginList(listed.stdout);
|
|
6004
|
+
if (!entry || !entry.installed) {
|
|
6005
|
+
return { installed: false, source: "`codex plugin list`", note: "caddis-codex plugin not installed" };
|
|
6006
|
+
}
|
|
6007
|
+
return {
|
|
6008
|
+
installed: true,
|
|
6009
|
+
version: entry.version,
|
|
6010
|
+
disabled: entry.disabled,
|
|
6011
|
+
source: "`codex plugin list`",
|
|
6012
|
+
note: entry.version ? void 0 : "installed but the version is not reported"
|
|
6013
|
+
};
|
|
6014
|
+
}
|
|
6015
|
+
function steps2(action) {
|
|
6016
|
+
if (action === "install") {
|
|
6017
|
+
return [
|
|
6018
|
+
{ cmd: BIN3, args: ["plugin", "marketplace", "add", MARKETPLACE_URL] },
|
|
6019
|
+
{ cmd: BIN3, args: ["plugin", "add", PLUGIN3] }
|
|
6020
|
+
];
|
|
6021
|
+
}
|
|
6022
|
+
return [
|
|
6023
|
+
{ cmd: BIN3, args: ["plugin", "marketplace", "upgrade", MARKETPLACE2] },
|
|
6024
|
+
{ cmd: BIN3, args: ["plugin", "add", PLUGIN3] }
|
|
6025
|
+
];
|
|
5910
6026
|
}
|
|
5911
6027
|
async function drive3(action, options) {
|
|
5912
6028
|
const detected = await detect3();
|
|
5913
6029
|
if (!detected.present) {
|
|
5914
6030
|
return { ok: true, skipped: true, steps: [], message: "codex not installed \u2014 skipped" };
|
|
5915
6031
|
}
|
|
5916
|
-
const bundle = bundlePath(BUNDLE2, { requireManifest: false });
|
|
5917
|
-
if (!bundle) {
|
|
5918
|
-
return {
|
|
5919
|
-
ok: false,
|
|
5920
|
-
skipped: false,
|
|
5921
|
-
steps: [],
|
|
5922
|
-
message: `the ${BUNDLE2} bundle is missing from this package \u2014 reinstall @caddis/cli`
|
|
5923
|
-
};
|
|
5924
|
-
}
|
|
5925
|
-
const source = path3.join(bundle, ".codex", "skills");
|
|
5926
|
-
if (!existsSync3(source)) {
|
|
5927
|
-
return {
|
|
5928
|
-
ok: false,
|
|
5929
|
-
skipped: false,
|
|
5930
|
-
steps: [],
|
|
5931
|
-
message: `the ${BUNDLE2} bundle has no .codex/skills directory \u2014 the export shape changed`
|
|
5932
|
-
};
|
|
5933
|
-
}
|
|
5934
6032
|
const home = os2.homedir();
|
|
5935
|
-
const
|
|
5936
|
-
const
|
|
5937
|
-
const
|
|
5938
|
-
const
|
|
6033
|
+
const legacyDir = caddisSkillsDir(home);
|
|
6034
|
+
const legacyMarker = versionFile(home);
|
|
6035
|
+
const hasLegacyMarker = existsSync3(legacyMarker);
|
|
6036
|
+
const planned = steps2(action);
|
|
6037
|
+
const removalCommand = `remove legacy ${shortenHome2(legacyDir)}`;
|
|
5939
6038
|
if (options.dryRun) {
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
};
|
|
6039
|
+
const plannedSteps = planned.map((step) => ({ command: formatCommand(step.cmd, step.args), ok: true, code: null }));
|
|
6040
|
+
if (hasLegacyMarker) {
|
|
6041
|
+
plannedSteps.push({ command: removalCommand, ok: true, code: null });
|
|
6042
|
+
}
|
|
6043
|
+
return { ok: true, skipped: true, steps: plannedSteps, message: "dry run \u2014 nothing executed" };
|
|
5946
6044
|
}
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
6045
|
+
const results = [];
|
|
6046
|
+
for (const step of planned) {
|
|
6047
|
+
const command = formatCommand(step.cmd, step.args);
|
|
6048
|
+
const result = await run(step.cmd, step.args);
|
|
6049
|
+
const isMarketplaceAdd = step.args[1] === "marketplace" && step.args[2] === "add";
|
|
6050
|
+
const isMarketplaceUpgrade = step.args[1] === "marketplace" && step.args[2] === "upgrade";
|
|
6051
|
+
let ok = result.ok;
|
|
6052
|
+
let code = result.code;
|
|
6053
|
+
if (!ok && isMarketplaceAdd) {
|
|
6054
|
+
const output = `${result.stdout}
|
|
6055
|
+
${result.stderr}`;
|
|
6056
|
+
if (/already (exists|added|configured|installed)/i.test(output)) {
|
|
6057
|
+
ok = true;
|
|
6058
|
+
code = 0;
|
|
6059
|
+
}
|
|
6060
|
+
}
|
|
6061
|
+
results.push({
|
|
6062
|
+
command,
|
|
6063
|
+
ok,
|
|
6064
|
+
code,
|
|
6065
|
+
output: ok ? void 0 : tail2(result.stderr || result.stdout || result.failure || "")
|
|
6066
|
+
});
|
|
6067
|
+
if (!ok && !isMarketplaceUpgrade) {
|
|
6068
|
+
return { ok: false, skipped: false, steps: results, message: `\`${command}\` failed` };
|
|
6069
|
+
}
|
|
5958
6070
|
}
|
|
5959
|
-
|
|
5960
|
-
for (const asset of ["AGENTS.md", path3.join(".codex", "config.toml.example")]) {
|
|
5961
|
-
const from = path3.join(bundle, asset);
|
|
5962
|
-
if (!existsSync3(from)) continue;
|
|
5963
|
-
const to = path3.join(dest, `caddis-${path3.basename(asset)}`);
|
|
6071
|
+
if (hasLegacyMarker) {
|
|
5964
6072
|
try {
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
} catch {
|
|
6073
|
+
rmSync2(legacyDir, { recursive: true, force: true });
|
|
6074
|
+
results.push({ command: removalCommand, ok: true, code: 0 });
|
|
6075
|
+
} catch (error) {
|
|
6076
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6077
|
+
results.push({ command: removalCommand, ok: false, code: null, output: message });
|
|
5968
6078
|
}
|
|
5969
6079
|
}
|
|
5970
|
-
const
|
|
5971
|
-
|
|
6080
|
+
const notes = [];
|
|
6081
|
+
if (!hasLegacyMarker && existsSync3(legacyDir)) {
|
|
6082
|
+
notes.push(
|
|
6083
|
+
`${shortenHome2(legacyDir)} exists but caddis did not write it (no version marker), so it was left in place; Codex may list the same skills twice. Remove it by hand if you do not need it.`
|
|
6084
|
+
);
|
|
6085
|
+
}
|
|
6086
|
+
if (options.extras) {
|
|
6087
|
+
notes.push("--extras has no Codex plugin; core only");
|
|
6088
|
+
}
|
|
5972
6089
|
return {
|
|
5973
6090
|
ok: true,
|
|
5974
6091
|
skipped: false,
|
|
5975
|
-
steps:
|
|
5976
|
-
|
|
6092
|
+
steps: results,
|
|
6093
|
+
...notes.length ? { message: notes.join(" ") } : {}
|
|
5977
6094
|
};
|
|
5978
6095
|
}
|
|
6096
|
+
function tail2(text, lines = 3) {
|
|
6097
|
+
return text.trim().split(/\r?\n/).slice(-lines).join("\n");
|
|
6098
|
+
}
|
|
5979
6099
|
var codexAdapter = {
|
|
5980
6100
|
id: "codex",
|
|
5981
6101
|
name: "Codex",
|
|
5982
6102
|
supported: true,
|
|
5983
|
-
summary: "
|
|
6103
|
+
summary: "install the caddis-codex plugin from the caddis marketplace (config.toml never merged)",
|
|
5984
6104
|
detect: detect3,
|
|
5985
6105
|
drive: drive3,
|
|
5986
6106
|
status: status3
|
|
@@ -5990,6 +6110,7 @@ var codexAdapter = {
|
|
|
5990
6110
|
import path4 from "path";
|
|
5991
6111
|
|
|
5992
6112
|
// src/agents/stub.ts
|
|
6113
|
+
init_which();
|
|
5993
6114
|
import { existsSync as existsSync4 } from "fs";
|
|
5994
6115
|
var NOT_YET_SUPPORTED = "not yet supported (v0.2)";
|
|
5995
6116
|
function createStubAdapter(spec) {
|
package/package.json
CHANGED