@caddis/cli 0.4.7 → 0.4.9
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/agents/preflight.md +1 -1
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/scripts/caddis_harness_path.py +132 -0
- package/bundles/antigravity-plugin/scripts/caddis_spawn.py +83 -17
- package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +13 -3
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +8 -1
- package/bundles/antigravity-plugin/skills/glossary/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/handoff/SKILL.md +18 -1
- package/bundles/antigravity-plugin/skills/kb/SKILL.md +21 -4
- package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +3 -3
- package/bundles/antigravity-plugin/skills/park/SKILL.md +32 -3
- package/bundles/antigravity-plugin/skills/prd/SKILL.md +7 -0
- package/bundles/antigravity-plugin/skills/setup-project-ai/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +2 -0
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +2 -0
- package/bundles/antigravity-plugin/skills/spawn-session/SKILL.md +44 -0
- package/bundles/antigravity-plugin/skills/statusline/SKILL.md +4 -4
- package/bundles/antigravity-plugin/skills/usage-review/SKILL.md +16 -13
- package/bundles/antigravity-plugin/skills/use-model/SKILL.md +1 -1
- package/bundles/manifest.json +2 -2
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ description: Use this agent to validate an implementation plan (or a phase of it
|
|
|
6
6
|
You are a plan-validation specialist. Your job: catch the plan's wrong assumptions **before** they
|
|
7
7
|
become wasted implementation. You diagnose; you do not fix and you do not write code.
|
|
8
8
|
|
|
9
|
-
## What to validate (
|
|
9
|
+
## What to validate (8 categories)
|
|
10
10
|
For the plan (or the named phase) check each claim against the codebase:
|
|
11
11
|
1. **File paths** — every file the plan says to create/edit: does the parent dir exist? Does an
|
|
12
12
|
existing file already cover it (collision/duplication)?
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Find the caddis SOURCE checkout from anywhere, so friction can be parked where it belongs.
|
|
3
|
+
|
|
4
|
+
Almost every insight about the harness is noticed while NOT working on the harness — a session in
|
|
5
|
+
an app repo hits friction and is in the wrong place to do anything about it. Today's options are
|
|
6
|
+
all bad: park it locally where the maintainer never looks, say it in chat where it dies with the
|
|
7
|
+
session, switch repos mid-task (nobody does), or remember it later (this is the one that actually
|
|
8
|
+
happens, and later is usually never).
|
|
9
|
+
|
|
10
|
+
So harness improvement is whatever someone recalls at the end of a session. The observations that
|
|
11
|
+
survive are not a random sample: small, frequent friction — the kind that would most improve daily
|
|
12
|
+
use — is exactly what does not survive, because it is not dramatic enough to recall.
|
|
13
|
+
|
|
14
|
+
This resolves the target so `/caddis:park --harness` is a flag rather than a context switch.
|
|
15
|
+
|
|
16
|
+
NOT the installed plugin. `CLAUDE_PLUGIN_ROOT` points at a read-only copy that is replaced on
|
|
17
|
+
every update; an item written there is gone at the next `claude plugin update`. The maintainer
|
|
18
|
+
works in the source checkout, so that is what this finds.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
EXIT_OK, EXIT_NOT_FOUND = 0, 1
|
|
28
|
+
|
|
29
|
+
ENV_VAR = "CADDIS_HARNESS_ROOT"
|
|
30
|
+
POINTER = Path.home() / ".caddis" / "harness-root"
|
|
31
|
+
|
|
32
|
+
# A source checkout has BOTH of these. The installed plugin has neither pair, and an app repo
|
|
33
|
+
# scaffolded by caddis has `.caddis/parking-lot/` but no `claude-harness/` — which is the
|
|
34
|
+
# distinction that matters, because writing into an app repo is the exact failure to avoid.
|
|
35
|
+
SIGNATURE = ("claude-harness", os.path.join(".caddis", "parking-lot"))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def looks_like_harness(path: Path) -> bool:
|
|
39
|
+
try:
|
|
40
|
+
return all((path / part).is_dir() for part in SIGNATURE)
|
|
41
|
+
except OSError:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def resolve_harness_root(cwd: Path | None = None,
|
|
46
|
+
env: dict | None = None,
|
|
47
|
+
pointer: Path | None = None) -> tuple[Path | None, str]:
|
|
48
|
+
"""(path, how-it-was-found) or (None, why-not).
|
|
49
|
+
|
|
50
|
+
Order is deliberate: an explicit answer beats a remembered one, and a remembered one beats a
|
|
51
|
+
guess. The search is last and is deliberately narrow.
|
|
52
|
+
"""
|
|
53
|
+
env = os.environ if env is None else env
|
|
54
|
+
pointer = POINTER if pointer is None else pointer
|
|
55
|
+
|
|
56
|
+
raw = (env.get(ENV_VAR) or "").strip()
|
|
57
|
+
if raw:
|
|
58
|
+
p = Path(raw).expanduser()
|
|
59
|
+
if looks_like_harness(p):
|
|
60
|
+
return p, f"{ENV_VAR}"
|
|
61
|
+
return None, (f"{ENV_VAR} is set to {raw!r}, which is not a caddis source checkout "
|
|
62
|
+
f"(needs both {SIGNATURE[0]}/ and {SIGNATURE[1]}/). Fix or unset it — an "
|
|
63
|
+
"explicit setting is never silently ignored.")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
noted = pointer.read_text(encoding="utf-8").strip()
|
|
67
|
+
except OSError:
|
|
68
|
+
noted = ""
|
|
69
|
+
if noted:
|
|
70
|
+
p = Path(noted).expanduser()
|
|
71
|
+
if looks_like_harness(p):
|
|
72
|
+
return p, str(pointer)
|
|
73
|
+
return None, (f"{pointer} points at {noted!r}, which is no longer a caddis source "
|
|
74
|
+
"checkout. Re-run with --set <path>.")
|
|
75
|
+
|
|
76
|
+
# Last resort: siblings of the current repo, then siblings of its parent. Every project on a
|
|
77
|
+
# given machine tends to live under one or two roots, so the checkout is usually a sibling of
|
|
78
|
+
# whatever repo noticed the friction. Deliberately shallow — a filesystem-wide search would
|
|
79
|
+
# be slow and would eventually find a stale clone.
|
|
80
|
+
start = (cwd or Path.cwd()).resolve()
|
|
81
|
+
seen: set[Path] = set()
|
|
82
|
+
for base in (start, *start.parents):
|
|
83
|
+
parent = base.parent
|
|
84
|
+
if parent in seen or parent == base:
|
|
85
|
+
continue
|
|
86
|
+
seen.add(parent)
|
|
87
|
+
try:
|
|
88
|
+
candidates = sorted(d for d in parent.iterdir() if d.is_dir())
|
|
89
|
+
except OSError:
|
|
90
|
+
continue
|
|
91
|
+
for d in candidates:
|
|
92
|
+
if looks_like_harness(d):
|
|
93
|
+
return d, f"found beside {base.name}"
|
|
94
|
+
if len(seen) >= 3:
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
return None, ("no caddis source checkout found. Set it once and it is remembered:"
|
|
98
|
+
f"{os.linesep} python caddis_harness_path.py --set <path-to-checkout>"
|
|
99
|
+
f"{os.linesep} or export {ENV_VAR}. NOT the installed plugin — an item written "
|
|
100
|
+
"there is discarded by the next `claude plugin update`.")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main(argv: list[str] | None = None) -> int:
|
|
104
|
+
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
105
|
+
ap.add_argument("--set", dest="set_to", default=None,
|
|
106
|
+
help="remember this path as the caddis source checkout")
|
|
107
|
+
ap.add_argument("--cwd", default=".", help="resolve as if from here")
|
|
108
|
+
a = ap.parse_args(argv)
|
|
109
|
+
|
|
110
|
+
if a.set_to:
|
|
111
|
+
p = Path(a.set_to).expanduser().resolve()
|
|
112
|
+
if not looks_like_harness(p):
|
|
113
|
+
sys.stderr.write(f"[harness] {p} is not a caddis source checkout — needs both "
|
|
114
|
+
f"{SIGNATURE[0]}/ and {SIGNATURE[1]}/{os.linesep}")
|
|
115
|
+
return EXIT_NOT_FOUND
|
|
116
|
+
POINTER.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
POINTER.write_text(str(p) + "\n", encoding="utf-8")
|
|
118
|
+
print(f"[harness] remembered {p} in {POINTER}")
|
|
119
|
+
return EXIT_OK
|
|
120
|
+
|
|
121
|
+
root, how = resolve_harness_root(Path(a.cwd).resolve())
|
|
122
|
+
if root is None:
|
|
123
|
+
# FAIL LOUDLY. Silently writing into the local repo is the failure this exists to prevent:
|
|
124
|
+
# the item would sit in an app repo the maintainer never reads, which is the status quo.
|
|
125
|
+
sys.stderr.write("[harness] " + how + os.linesep)
|
|
126
|
+
return EXIT_NOT_FOUND
|
|
127
|
+
print(str(root))
|
|
128
|
+
return EXIT_OK
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
raise SystemExit(main())
|
|
@@ -470,8 +470,17 @@ def capture_order(repo_root: Path, spawn_id: str) -> tuple[list[str], list[str]]
|
|
|
470
470
|
CHASE_AFTER_S = 1800
|
|
471
471
|
|
|
472
472
|
# Ordered. Each event may only move the handshake forward.
|
|
473
|
-
|
|
474
|
-
|
|
473
|
+
# Ordered. An event may only move the handshake FORWARD along this list.
|
|
474
|
+
#
|
|
475
|
+
# Two entry points, one list. PARENT-FIRST (the product owner starts the child, then names it)
|
|
476
|
+
# begins at `awaiting-readback`, because the parent speaks first and must not take the answers on
|
|
477
|
+
# trust. CHILD-FIRST (008's original: print a prompt, a human pastes it) begins at
|
|
478
|
+
# `awaiting-answers`, because there the child's first message is itself the evidence of reading
|
|
479
|
+
# and a read-back would be asking for something already proven.
|
|
480
|
+
HANDSHAKE_STATES = ("awaiting-readback", "awaiting-answers", "answered", "verdict-sent",
|
|
481
|
+
"acknowledged")
|
|
482
|
+
_EVENT_STATE = {"readback": "awaiting-answers", "answers": "answered",
|
|
483
|
+
"verdict": "verdict-sent", "ack": "acknowledged"}
|
|
475
484
|
|
|
476
485
|
|
|
477
486
|
def detect_transport(env: dict | None = None) -> tuple[str, str]:
|
|
@@ -501,7 +510,8 @@ def _handshake_path(repo_root: Path, spawn_id: str) -> Path:
|
|
|
501
510
|
return repo_root / SPAWN_DIR / (spawn_id + "-handshake.json")
|
|
502
511
|
|
|
503
512
|
|
|
504
|
-
def handshake_open(repo_root: Path, spawn_id: str, transport: str = "auto"
|
|
513
|
+
def handshake_open(repo_root: Path, spawn_id: str, transport: str = "auto",
|
|
514
|
+
peer: str = "") -> dict:
|
|
505
515
|
"""Record that a handover was issued. Written by the PARENT, at the moment it prints the
|
|
506
516
|
prompt — not when the child replies, because the whole point is to make an unanswered
|
|
507
517
|
handshake visible."""
|
|
@@ -510,9 +520,16 @@ def handshake_open(repo_root: Path, spawn_id: str, transport: str = "auto") -> d
|
|
|
510
520
|
chosen, why = transport, "forced with --transport"
|
|
511
521
|
state = {
|
|
512
522
|
"id": spawn_id,
|
|
513
|
-
|
|
523
|
+
# Parent-first only when we can actually reach the child: a name AND a transport.
|
|
524
|
+
"state": HANDSHAKE_STATES[0] if (peer and chosen == "message") else HANDSHAKE_STATES[1],
|
|
514
525
|
"transport": chosen,
|
|
515
526
|
"transport_reason": why,
|
|
527
|
+
# HOW TO ADDRESS THE CHILD. `SendMessage` takes the name `ListAgents` shows, and until
|
|
528
|
+
# 2026-09-08 this file recorded none — so the parent knew a handshake was open and had no
|
|
529
|
+
# idea who to send the chase or the verdict to. Usually EMPTY at open: the child does not
|
|
530
|
+
# exist yet. The parent fills it in from the `from-name` of the child's first message,
|
|
531
|
+
# which is the only moment either side reliably knows that name.
|
|
532
|
+
"peer": peer,
|
|
516
533
|
"opened_at": int(time.time()),
|
|
517
534
|
"history": [],
|
|
518
535
|
}
|
|
@@ -529,7 +546,8 @@ def handshake_read(repo_root: Path, spawn_id: str) -> dict | None:
|
|
|
529
546
|
return None
|
|
530
547
|
|
|
531
548
|
|
|
532
|
-
def handshake_record(repo_root: Path, spawn_id: str, event: str
|
|
549
|
+
def handshake_record(repo_root: Path, spawn_id: str, event: str,
|
|
550
|
+
peer: str = "") -> tuple[dict | None, str]:
|
|
533
551
|
"""Advance the handshake. Never rewinds: a verdict cannot un-answer the questions, and an
|
|
534
552
|
out-of-order event is a sign the operator has lost track of which round they are in — which
|
|
535
553
|
is exactly when a silent overwrite would hurt."""
|
|
@@ -537,17 +555,48 @@ def handshake_record(repo_root: Path, spawn_id: str, event: str) -> tuple[dict |
|
|
|
537
555
|
if state is None:
|
|
538
556
|
return None, f"no handshake for {spawn_id} — run `handshake open --id {spawn_id}` first"
|
|
539
557
|
target = _EVENT_STATE[event]
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
558
|
+
here = HANDSHAKE_STATES.index(state["state"])
|
|
559
|
+
there = HANDSHAKE_STATES.index(target)
|
|
560
|
+
# EXACTLY one step. Monotonic-only let `answers` jump straight over `awaiting-readback`,
|
|
561
|
+
# which silently skipped the read-back gate — the one thing the parent-first flow adds.
|
|
562
|
+
# Backwards is an operator who has lost track of the round; forwards-by-two is a gate not run.
|
|
563
|
+
if there != here + 1:
|
|
564
|
+
expected = HANDSHAKE_STATES[here + 1] if here + 1 < len(HANDSHAKE_STATES) else "nothing"
|
|
565
|
+
wanted = next((e for e, st in _EVENT_STATE.items() if st == expected), "—")
|
|
566
|
+
return state, (f"at `{state['state']}`, so the only event that fits is `{wanted}` "
|
|
567
|
+
f"(-> `{expected}`). `{event}` would "
|
|
568
|
+
+ ("skip a step" if there > here + 1 else "move backwards")
|
|
569
|
+
+ " — refusing rather than rewriting, because a skipped step here is a "
|
|
570
|
+
"gate that never ran.")
|
|
571
|
+
if peer:
|
|
572
|
+
state["peer"] = peer
|
|
573
|
+
state["history"].append({"event": event, "at": int(time.time()),
|
|
574
|
+
**({"peer": peer} if peer else {})})
|
|
545
575
|
state["state"] = target
|
|
546
576
|
_handshake_path(repo_root, spawn_id).write_text(
|
|
547
577
|
json.dumps(state, indent=2) + chr(10), encoding="utf-8")
|
|
548
578
|
return state, ""
|
|
549
579
|
|
|
550
580
|
|
|
581
|
+
def chase_instruction(state: dict) -> str:
|
|
582
|
+
"""What to actually DO when a chase is due — addressed if we can, honest if we cannot."""
|
|
583
|
+
peer = (state.get("peer") or "").strip()
|
|
584
|
+
if state.get("state") == HANDSHAKE_STATES[0] and peer:
|
|
585
|
+
return (f'No read-back from "{peer}". It was messaged and has not said what it READ, which '
|
|
586
|
+
"is the cheapest signal that it skipped the gate and started working. Ask again, "
|
|
587
|
+
"and ask for file paths — a summary can be written without opening anything.")
|
|
588
|
+
if state.get("transport") != "message":
|
|
589
|
+
return ("This handshake is on the paste route, so the chase goes through the human. Say "
|
|
590
|
+
"so explicitly — otherwise they watch nothing happen and conclude it is broken.")
|
|
591
|
+
if peer:
|
|
592
|
+
return f'SendMessage to: "{peer}" — carry TWO FRESH questions, not just a reminder.'
|
|
593
|
+
return ("No peer address recorded, so the child has not made contact yet — which is itself "
|
|
594
|
+
"the signal. Run `ListAgents` and look for a session in this repo; a session started "
|
|
595
|
+
"seconds ago may be absent from one listing, so a single empty result is not proof it "
|
|
596
|
+
"does not exist. Once it replies, capture the name: "
|
|
597
|
+
"`handshake record --id <id> --event answers --peer <from-name>`.")
|
|
598
|
+
|
|
599
|
+
|
|
551
600
|
def handshake_chase_due(state: dict, now: int | None = None) -> int:
|
|
552
601
|
"""Seconds overdue for a chase, or 0. Only meaningful while awaiting answers.
|
|
553
602
|
|
|
@@ -555,7 +604,7 @@ def handshake_chase_due(state: dict, now: int | None = None) -> int:
|
|
|
555
604
|
pre-read in the handover file — a stronger test than the original set, and free, because the
|
|
556
605
|
parent is holding the context anyway.
|
|
557
606
|
"""
|
|
558
|
-
if state.get("state")
|
|
607
|
+
if state.get("state") not in (HANDSHAKE_STATES[0], HANDSHAKE_STATES[1]):
|
|
559
608
|
return 0
|
|
560
609
|
now = int(time.time()) if now is None else now
|
|
561
610
|
return max(0, now - int(state.get("opened_at", now)) - CHASE_AFTER_S)
|
|
@@ -611,6 +660,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
611
660
|
h.add_argument("--repo-root", default=".")
|
|
612
661
|
h.add_argument("--event", choices=sorted(_EVENT_STATE))
|
|
613
662
|
h.add_argument("--transport", choices=["auto", "message", "paste"], default="auto")
|
|
663
|
+
h.add_argument("--peer", default="",
|
|
664
|
+
help="the child's name as ListAgents shows it — take it from the `from-name` "
|
|
665
|
+
"of its first message; that is the only moment it is reliably known")
|
|
614
666
|
q = sub.add_parser("verify-question")
|
|
615
667
|
q.add_argument("--answer-in", required=True)
|
|
616
668
|
q.add_argument("--needle", required=True)
|
|
@@ -677,9 +729,17 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
677
729
|
|
|
678
730
|
if a.cmd == "handshake":
|
|
679
731
|
if a.action == "open":
|
|
680
|
-
st = handshake_open(root, a.id, a.transport)
|
|
732
|
+
st = handshake_open(root, a.id, a.transport, a.peer)
|
|
681
733
|
print(f"[spawn] handshake {a.id} open — transport `{st['transport']}` "
|
|
682
|
-
f"({st['transport_reason']})")
|
|
734
|
+
f"({st['transport_reason']}), state `{st['state']}`")
|
|
735
|
+
if st["state"] == HANDSHAKE_STATES[0]:
|
|
736
|
+
print(f" PARENT-FIRST. You have the address, so message \"{st['peer']}\" now: "
|
|
737
|
+
"tell it what to read, then ask it to reply with WHICH FILES it opened "
|
|
738
|
+
"before it answers anything. Record that with `--event readback`.")
|
|
739
|
+
elif st["transport"] == "message" and not st["peer"]:
|
|
740
|
+
print(" No --peer given, so this is the child-first flow: print the prompt and "
|
|
741
|
+
"wait. If you already started the child, re-open with `--peer <its "
|
|
742
|
+
"ListAgents name>` to message it directly instead.")
|
|
683
743
|
if st["transport"] == "paste":
|
|
684
744
|
print(" The child cannot message you. Print the prompt for the human to carry, "
|
|
685
745
|
"and say so — otherwise they watch nothing happen and conclude it is "
|
|
@@ -691,23 +751,29 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
691
751
|
return EXIT_REFUSED
|
|
692
752
|
if a.action == "status":
|
|
693
753
|
over = handshake_chase_due(st)
|
|
694
|
-
|
|
754
|
+
peer = st.get("peer") or "(not recorded — see below)"
|
|
755
|
+
print(f"[spawn] {a.id}: {st['state']} via {st['transport']}, peer {peer}")
|
|
695
756
|
if over:
|
|
696
757
|
print(f" CHASE DUE — {over}s past the {CHASE_AFTER_S}s window. Silence is not "
|
|
697
758
|
"success: a child that skipped the gate and started work looks exactly "
|
|
698
|
-
"like one still reading.
|
|
699
|
-
|
|
759
|
+
"like one still reading.")
|
|
760
|
+
print(" " + chase_instruction(st))
|
|
700
761
|
return EXIT_OK
|
|
701
762
|
if a.action == "record":
|
|
702
763
|
if not a.event:
|
|
703
764
|
sys.stderr.write("[spawn] --event is required for `record`" + chr(10))
|
|
704
765
|
return EXIT_REFUSED
|
|
705
|
-
st, err = handshake_record(root, a.id, a.event)
|
|
766
|
+
st, err = handshake_record(root, a.id, a.event, a.peer)
|
|
706
767
|
if err:
|
|
707
768
|
sys.stderr.write("[spawn] " + err + chr(10))
|
|
708
769
|
return EXIT_REFUSED
|
|
709
770
|
print(f"[spawn] {a.id}: {st['state']}")
|
|
710
771
|
return EXIT_OK
|
|
772
|
+
if a.action == "close" and not (st.get("peer") or "").strip() \
|
|
773
|
+
and st["state"] != HANDSHAKE_STATES[0]:
|
|
774
|
+
# Contact happened but nobody wrote down who with. The verdict has to reach someone.
|
|
775
|
+
print(" note no peer recorded although the handshake progressed — re-run "
|
|
776
|
+
"`record --peer <from-name>` so the next session can see who this was with.")
|
|
711
777
|
ok, msg = handshake_close(root, a.id)
|
|
712
778
|
(print if ok else sys.stderr.write)("[spawn] " + msg + ("" if ok else chr(10)))
|
|
713
779
|
return EXIT_OK if ok else EXIT_REFUSED
|
|
@@ -64,9 +64,19 @@ fi
|
|
|
64
64
|
> had been fixed on 2026-08-01 — the fix just never reached the caller, because a file someone
|
|
65
65
|
> copied weeks earlier silently won. `caddis_gate.py vendor-drift` now fails on the same condition.
|
|
66
66
|
```
|
|
67
|
-
python "$TOOL"
|
|
68
|
-
python "$TOOL"
|
|
69
|
-
|
|
67
|
+
python "$TOOL" $ARGUMENTS # pass the user's flags THROUGH, raw
|
|
68
|
+
python "$TOOL" # no arguments: DeepSeek eyes on the working tree
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**`$ARGUMENTS` goes through raw, and must not be re-prefixed.** Callers are told to type the flag
|
|
72
|
+
themselves — `feature-plan.md` emits `/caddis:cross-review --range main..HEAD` into every plan it
|
|
73
|
+
writes. This line used to prefix the flag itself, which turned that into a doubled
|
|
74
|
+
`--range --range main..HEAD` and made every plan-emitted call fail. Whatever the user typed is already a complete flag list:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
/caddis:cross-review -> the working tree, DeepSeek
|
|
78
|
+
/caddis:cross-review --range main..HEAD -> that range
|
|
79
|
+
/caddis:cross-review --provider glm -> GLM eyes
|
|
70
80
|
```
|
|
71
81
|
Optional: `--base-url <url>`, `--model <id>` (env always overrides the preset).
|
|
72
82
|
|
|
@@ -22,6 +22,13 @@ description**, and there may be **no codebase, PROJECT-FACTS, or prior PRD** to
|
|
|
22
22
|
write a complete best-effort plan anyway.
|
|
23
23
|
|
|
24
24
|
Absolute rules in this mode (they override everything below):
|
|
25
|
+
|
|
26
|
+
> **The same policy exists in `/caddis:prd`, and the two are deliberately NOT identical.**
|
|
27
|
+
> Each is specialised to the artefact its command writes — where the output goes, and
|
|
28
|
+
> where an unresolved decision is recorded. **Do not merge them, and do not replace
|
|
29
|
+
> either with a pointer:** headless mode is precisely when no human is present and the
|
|
30
|
+
> session will not go and read another command to find its own rules.
|
|
31
|
+
|
|
25
32
|
- **NEVER ask a question, request clarification, or end your turn with questions.** Replying with a list
|
|
26
33
|
of scoping questions is a hard failure. Never use AskUserQuestion, never pause, never wait for input.
|
|
27
34
|
- **A bare title (or PRD path) is sufficient input.** Where information is missing, invent a reasonable,
|
|
@@ -112,7 +119,7 @@ escalation, never a planned default.
|
|
|
112
119
|
|
|
113
120
|
### Assign an execution LANE per phase (which harness runs it — not just which model)
|
|
114
121
|
A tier can be served by an OSS provider instead of Anthropic (cheat sheet:
|
|
115
|
-
`docs/guide/multi-model-workflow.md
|
|
122
|
+
`docs/guide/multi-model-workflow.md` (in the caddis **source** repo — `docs/` is not shipped inside the installed plugin); wiring: the caddis repo's `.caddis/plans/model-access.md`):
|
|
116
123
|
- **claude** (default) — this session, `/model <alias>` as above. Required for judgment-heavy,
|
|
117
124
|
security-sensitive, or novel-architecture phases.
|
|
118
125
|
- **glm-headless** — mechanical, fully-specced phases run on GLM without touching Claude quota:
|
|
@@ -15,7 +15,7 @@ Your job is the half a script cannot do: **write what the words mean here.**
|
|
|
15
15
|
## Step 1 — get the candidates
|
|
16
16
|
|
|
17
17
|
```
|
|
18
|
-
python
|
|
18
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_glossary.py"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
It ranks the words this repo says often that plain English does not, and marks which already
|
|
@@ -26,7 +26,7 @@ is to run it.
|
|
|
26
26
|
git status --short
|
|
27
27
|
git branch --show-current
|
|
28
28
|
git log --oneline -5
|
|
29
|
-
python scripts/caddis_inventory.py # what EXISTS — do not compose this from memory
|
|
29
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_inventory.py" # what EXISTS — do not compose this from memory
|
|
30
30
|
```
|
|
31
31
|
Then read the active plan in `.caddis/plans/` (falling back to legacy `.github/plans/` if present) and its tracker.
|
|
32
32
|
|
|
@@ -85,6 +85,23 @@ had developed by then.
|
|
|
85
85
|
|
|
86
86
|
Record the outcome in relay.md's `## Learnings captured` line (below).
|
|
87
87
|
|
|
88
|
+
## Step 2c — was any of it the harness?
|
|
89
|
+
|
|
90
|
+
<!-- shared:harness-friction-question — keep byte-identical across commands; a test pins it -->
|
|
91
|
+
**One question, asked here because here is where you are already reflecting.** The observations
|
|
92
|
+
that reach caddis today are the ones someone happened to recall days later — so small, frequent
|
|
93
|
+
friction never arrives, which is exactly the friction worth fixing.
|
|
94
|
+
|
|
95
|
+
If a command was awkward, a gate fired wrongly, a doc sent you the wrong way: park it against the
|
|
96
|
+
harness, not this repo.
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
/caddis:park --harness <what was awkward, and what you expected>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Nothing to say is the normal answer. Say nothing and move on — this must not become a ritual.
|
|
103
|
+
<!-- /shared:harness-friction-question -->
|
|
104
|
+
|
|
88
105
|
## Step 3 — write the resume doc (overwrite) with exactly these sections
|
|
89
106
|
> **Where to write:** solo / single active branch → `.caddis/relay.md` (default).
|
|
90
107
|
> **Team / parallel branches** → write `.caddis/relay/<current-branch>.md` instead, so two
|
|
@@ -15,7 +15,7 @@ It ships with the harness. Try, in order:
|
|
|
15
15
|
|
|
16
16
|
## Step 2 — run the reindexer
|
|
17
17
|
```
|
|
18
|
-
python
|
|
18
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/check_doc_coverage.py" --reindex
|
|
19
19
|
```
|
|
20
20
|
It is **additive and safe** — never deletes your rows:
|
|
21
21
|
- **Missing map** → creates `.caddis/kb/DOC-MAP.md` from a scaffold, pre-linking the repo's obvious
|
|
@@ -32,18 +32,35 @@ Read the `[kb]` summary it printed, then:
|
|
|
32
32
|
- The note is **gone for good** → remove its row. To clear all dangling rows at once, use the
|
|
33
33
|
destructive opt-in — but **show the dangling list and confirm with the user first**:
|
|
34
34
|
```
|
|
35
|
-
python
|
|
35
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/check_doc_coverage.py" --prune
|
|
36
36
|
```
|
|
37
37
|
`--prune` removes *only* index rows that link to missing files (never valid rows, never prose),
|
|
38
38
|
and still indexes any orphan notes in the same run.
|
|
39
39
|
|
|
40
40
|
## Step 4 — verify clean
|
|
41
41
|
```
|
|
42
|
-
python
|
|
42
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/check_doc_coverage.py" --check
|
|
43
43
|
```
|
|
44
44
|
Exit `0` = the index is honest (no dangling links; every note indexed). The SessionStart hook will now
|
|
45
45
|
surface the `[DOC-MAP]` "read the index first" pointer for future sessions in this repo.
|
|
46
46
|
|
|
47
|
+
## Step 5 — was any of it the harness?
|
|
48
|
+
|
|
49
|
+
<!-- shared:harness-friction-question — keep byte-identical across commands; a test pins it -->
|
|
50
|
+
**One question, asked here because here is where you are already reflecting.** The observations
|
|
51
|
+
that reach caddis today are the ones someone happened to recall days later — so small, frequent
|
|
52
|
+
friction never arrives, which is exactly the friction worth fixing.
|
|
53
|
+
|
|
54
|
+
If a command was awkward, a gate fired wrongly, a doc sent you the wrong way: park it against the
|
|
55
|
+
harness, not this repo.
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
/caddis:park --harness <what was awkward, and what you expected>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Nothing to say is the normal answer. Say nothing and move on — this must not become a ritual.
|
|
62
|
+
<!-- /shared:harness-friction-question -->
|
|
63
|
+
|
|
47
64
|
## KB note format (OKF-lite — mandatory for every new note)
|
|
48
65
|
|
|
49
66
|
Every new `.caddis/kb/*.md` note starts with this frontmatter block. **`type` is the only required
|
|
@@ -68,7 +85,7 @@ generated: { by: caddis/<model-id>, at: 2026-07-28T09:00:00Z }
|
|
|
68
85
|
|
|
69
86
|
The full schema, the caddis↔OKF `status` mapping, and what caddis deliberately skips from OKF v0.2
|
|
70
87
|
(`sources:`, `Attested Computation`) live in
|
|
71
|
-
|
|
88
|
+
`.github/instructions/document-frontmatter.instructions.md` in the caddis source repo. (Not a link: a relative path out of the plugin directory resolves nowhere once installed.)
|
|
72
89
|
|
|
73
90
|
**Backward compatible:** an existing note with no trust fields is still correct. Do not bulk-migrate
|
|
74
91
|
old notes — add the fields as notes are next touched.
|
|
@@ -29,7 +29,7 @@ pass `--allow-dirty` on a tree whose changes you didn't make.
|
|
|
29
29
|
|
|
30
30
|
## Step 3 — dry run
|
|
31
31
|
```
|
|
32
|
-
python
|
|
32
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_migrate_dir.py" <target>
|
|
33
33
|
```
|
|
34
34
|
Read the plan it prints. Two shapes:
|
|
35
35
|
- **Simple** — only `.claudster/` exists → one `git mv .claudster .caddis` (history preserved).
|
|
@@ -42,7 +42,7 @@ Show the user the dry-run output before applying.
|
|
|
42
42
|
|
|
43
43
|
## Step 4 — apply
|
|
44
44
|
```
|
|
45
|
-
python
|
|
45
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_migrate_dir.py" <target> --apply
|
|
46
46
|
```
|
|
47
47
|
It also rewrites `.claudster/…` path references inside the repo's **live** state — `.caddis/relay.md`
|
|
48
48
|
and `.caddis/workstreams.json` (parked-workstream plan paths). Historical artifacts (past plans,
|
|
@@ -53,7 +53,7 @@ Resolve any CONFLICT it reported by hand, then re-run — the command is idempot
|
|
|
53
53
|
## Step 5 — verify, then commit
|
|
54
54
|
```
|
|
55
55
|
git -C <target> status --short # renames should show as R (history preserved)
|
|
56
|
-
python
|
|
56
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_migrate_dir.py" <target> --check # exit 0 = no legacy dir left
|
|
57
57
|
```
|
|
58
58
|
Then run the repo's own gate (its tests, and `check_doc_coverage.py --check` if present) and commit
|
|
59
59
|
the rename **on its own**, e.g. `chore(caddis): migrate .claudster/ -> .caddis/`.
|
|
@@ -20,12 +20,41 @@ The request is **$ARGUMENTS**.
|
|
|
20
20
|
|
|
21
21
|
| `$ARGUMENTS` | Do this |
|
|
22
22
|
|---|---|
|
|
23
|
+
| starts with `--harness` | Go to **Harness mode** — the item is about caddis, not this repo |
|
|
23
24
|
| empty or `list` | Go to **List mode** |
|
|
24
25
|
| `done <slug>` or `drop <slug>` | Go to **Close mode** |
|
|
25
26
|
| anything else | Go to **File mode** — the text is what to park |
|
|
26
27
|
|
|
27
28
|
---
|
|
28
29
|
|
|
30
|
+
## Harness mode — `--harness`
|
|
31
|
+
|
|
32
|
+
**Use it when the friction was caused by the harness, not the project.** You are almost certainly
|
|
33
|
+
in an app repo right now, because that is where harness friction is noticed. Parking it here sends
|
|
34
|
+
it somewhere the maintainer never reads.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_harness_path.py"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Prints the caddis SOURCE checkout, or **fails with exit 1 and says how to set it**. Write the item
|
|
41
|
+
to `<that path>/.caddis/parking-lot/<slug>.md`, following File mode below, plus:
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
scope: harness # so a later sweep can find it mechanically
|
|
45
|
+
found-by: <this repo> / session <id>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**On a non-zero exit, STOP and show the message.** Do not fall back to the local repo — an item
|
|
49
|
+
filed there is the status quo this flag exists to end. Setting it is one command and is remembered.
|
|
50
|
+
|
|
51
|
+
**Never commit it.** Capture is cheap and safe; deciding is not. Leave the file for the maintainer.
|
|
52
|
+
|
|
53
|
+
Not the installed plugin: `CLAUDE_PLUGIN_ROOT` is a read-only copy replaced on every
|
|
54
|
+
`claude plugin update`, so an item written there is discarded.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
29
58
|
## File mode
|
|
30
59
|
|
|
31
60
|
### Step 1 — check it belongs here
|
|
@@ -88,7 +117,7 @@ Rules the gate enforces — get them right the first time:
|
|
|
88
117
|
Run the gate and show the result:
|
|
89
118
|
|
|
90
119
|
```bash
|
|
91
|
-
python scripts/caddis_gate.py parking-lot --repo-root .
|
|
120
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_gate.py" parking-lot --repo-root .
|
|
92
121
|
```
|
|
93
122
|
|
|
94
123
|
Exit 0 means it conforms. Exit 1 means fix what it printed, then re-run. Do not report the item as
|
|
@@ -109,7 +138,7 @@ Read every `.caddis/parking-lot/*.md` (skip `README.md` and `done/`). Print one
|
|
|
109
138
|
|---|---|---|---|---|
|
|
110
139
|
|
|
111
140
|
Below the table, print the counts: how many open, how many committed (`future: yes`). Then run
|
|
112
|
-
`python scripts/caddis_gate.py parking-lot --repo-root .` and report any violation it prints.
|
|
141
|
+
`python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_gate.py" parking-lot --repo-root .` and report any violation it prints.
|
|
113
142
|
|
|
114
143
|
If the directory is empty, say so in one line. Do not invent items.
|
|
115
144
|
|
|
@@ -122,5 +151,5 @@ If the directory is empty, say so in one line. Do not invent items.
|
|
|
122
151
|
3. **For `dropped`, append a `## Why dropped` section.** This is not optional. An item deleted with
|
|
123
152
|
no reason gets re-raised by the next session that has the same idea; an item kept with "no,
|
|
124
153
|
because ..." does not.
|
|
125
|
-
4. Run `python scripts/caddis_tidy.py --apply`, which moves it into `.caddis/parking-lot/done/`.
|
|
154
|
+
4. Run `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_tidy.py" --apply`, which moves it into `.caddis/parking-lot/done/`.
|
|
126
155
|
5. Report the file's new path in one line.
|
|
@@ -30,6 +30,13 @@ forbidden**. This mode exists for one-line ideas: the card may be nothing more t
|
|
|
30
30
|
fully actionable — a terse title is enough to write a complete draft PRD.
|
|
31
31
|
|
|
32
32
|
Absolute rules in this mode (they override everything above):
|
|
33
|
+
|
|
34
|
+
> **The same policy exists in `/caddis:feature-plan`, and the two are deliberately NOT identical.**
|
|
35
|
+
> Each is specialised to the artefact its command writes — where the output goes, and
|
|
36
|
+
> where an unresolved decision is recorded. **Do not merge them, and do not replace
|
|
37
|
+
> either with a pointer:** headless mode is precisely when no human is present and the
|
|
38
|
+
> session will not go and read another command to find its own rules.
|
|
39
|
+
|
|
33
40
|
- **NEVER ask a question. NEVER request clarification. NEVER end your turn with questions.** Replying with
|
|
34
41
|
something like *"a few questions to scope this before I write the PRD"* is a hard failure. Never use
|
|
35
42
|
AskUserQuestion, never pause for approval, never wait for input.
|
|
@@ -35,7 +35,7 @@ alias claude-glm="<path-to>/claude-harness/scripts/claude-oss.sh"
|
|
|
35
35
|
```
|
|
36
36
|
Resolve `<path-to>` per the same plugin-vs-source rule as the generator itself (`${CLAUDE_PLUGIN_ROOT}`
|
|
37
37
|
for a plugin install, the harness checkout path for caddis). Also set `CADDIS_KEYS_FILE`
|
|
38
|
-
(default `~/.caddis/keys.env`) with the provider keys — see `docs/guide/providers-and-keys.md
|
|
38
|
+
(default `~/.caddis/keys.env`) with the provider keys — see `docs/guide/providers-and-keys.md` (in the caddis **source** repo — `docs/` is not shipped inside the installed plugin).
|
|
39
39
|
|
|
40
40
|
## After the deterministic step — deploy private harness skills (optional)
|
|
41
41
|
|
|
@@ -46,6 +46,7 @@ Use conventional commits: `fix:`, `feat:`, `chore:`, `refactor:`, `docs:`. Scope
|
|
|
46
46
|
|
|
47
47
|
**2b. CROSS-REVIEW TRIGGER** — `ship` has no review pause at all, which is how sixteen fixes reached production unreviewed in one day.
|
|
48
48
|
|
|
49
|
+
<!-- shared:cross-review-trigger — keep byte-identical across commands; a test pins it -->
|
|
49
50
|
**Run the trigger, do not eyeball the diff:**
|
|
50
51
|
|
|
51
52
|
```bash
|
|
@@ -60,6 +61,7 @@ gets switched off inside a week.
|
|
|
60
61
|
It fires on SQL and repositories, `services/`, caches and refresh jobs, auth and RBAC, or a diff
|
|
61
62
|
over 400 changed lines. No judgement in any of those — deliberately, because judgement is exactly
|
|
62
63
|
what failed.
|
|
64
|
+
<!-- /shared:cross-review-trigger -->
|
|
63
65
|
|
|
64
66
|
**3. PUSH** — to the repo's default branch (confirm the branch first; don't assume `main`):
|
|
65
67
|
```
|
|
@@ -52,6 +52,7 @@ are uncommitted.)
|
|
|
52
52
|
|
|
53
53
|
## Step 3b — Cross-review trigger (before anything is pushed)
|
|
54
54
|
|
|
55
|
+
<!-- shared:cross-review-trigger — keep byte-identical across commands; a test pins it -->
|
|
55
56
|
**Run the trigger, do not eyeball the diff:**
|
|
56
57
|
|
|
57
58
|
```bash
|
|
@@ -66,6 +67,7 @@ gets switched off inside a week.
|
|
|
66
67
|
It fires on SQL and repositories, `services/`, caches and refresh jobs, auth and RBAC, or a diff
|
|
67
68
|
over 400 changed lines. No judgement in any of those — deliberately, because judgement is exactly
|
|
68
69
|
what failed.
|
|
70
|
+
<!-- /shared:cross-review-trigger -->
|
|
69
71
|
|
|
70
72
|
> **Why this is mechanical and not "consider a review here".** serve-sight shipped four
|
|
71
73
|
> production releases in one day — sixteen fixes across SQL, caches, refresh jobs and React — and
|
|
@@ -24,6 +24,36 @@ A long session does not forget. **It recalls superseded facts fluently.** Measur
|
|
|
24
24
|
Each was stated with conviction and was wrong. Asking that agent what it knows cannot find any of
|
|
25
25
|
it.
|
|
26
26
|
|
|
27
|
+
## Start the successor FIRST, then name it
|
|
28
|
+
|
|
29
|
+
**Preferred flow.** Start the child with a name you choose, then pass that same name here:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
claude --name successor-9c2 # in a new terminal
|
|
33
|
+
/caddis:spawn-session successor-9c2 finish the harvester # here
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`claude -n/--name` sets the session's display name, and that is exactly the name `SendMessage`
|
|
37
|
+
addresses. You pick it, so nothing has to be looked up.
|
|
38
|
+
|
|
39
|
+
**Check the row before you trust the name.** Run `ListAgents` once and confirm the child is there
|
|
40
|
+
and reads `interactive`, not `offline` — a listing carries long-dead sessions, so presence alone
|
|
41
|
+
proves nothing. Names are also **not unique**: if two rows share one, `SendMessage` needs the
|
|
42
|
+
` [ref]` shown beside it. Pick a short distinct name and this never arises.
|
|
43
|
+
|
|
44
|
+
**Why this ordering.** With the child already running you hold its address from the start, so the
|
|
45
|
+
parent messages it directly and nothing has to be pasted. The older flow printed a prompt for a
|
|
46
|
+
human to carry, which meant the child did not exist yet and its address could not be known.
|
|
47
|
+
|
|
48
|
+
**What you give up, and how it is replaced.** In the older flow the child spoke first, so its
|
|
49
|
+
first message was itself proof it had read the handover. Here the parent speaks first, so that
|
|
50
|
+
proof is a separate step: **your opening message asks the child to reply with WHICH FILES it
|
|
51
|
+
opened, before it answers anything.** Ask for paths — a summary can be written without opening
|
|
52
|
+
anything. Record it with `--event readback`; until you do, the handshake refuses `--event answers`.
|
|
53
|
+
|
|
54
|
+
**No name given?** The command falls back to the older printed-prompt flow. That is also the only
|
|
55
|
+
route on agy and Codex, which have no `SendMessage`.
|
|
56
|
+
|
|
27
57
|
The successor is what to pick up first: **$ARGUMENTS** (if empty, derive it from the parking-lot
|
|
28
58
|
and the active plan).
|
|
29
59
|
|
|
@@ -242,6 +272,20 @@ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_spawn.py" handshake status --id <id
|
|
|
242
272
|
Two questions asked at chase time cannot have been pre-read in the handover, which is a stronger
|
|
243
273
|
test than the original set — and it costs nothing, since you are holding the context anyway.
|
|
244
274
|
|
|
275
|
+
**Write down HOW to reach the child the moment it first messages you.** `SendMessage` addresses a
|
|
276
|
+
peer by the name `ListAgents` shows, and you cannot know that name before the child exists — it
|
|
277
|
+
arrives as the `from-name` on its first message. Capture it there or you will be holding an open
|
|
278
|
+
handshake with no way to send the chase or the verdict:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
... handshake record --id <id> --event answers --peer <from-name>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`handshake status` then prints the address, so a chase is a copy-paste. With no peer recorded it
|
|
285
|
+
says so plainly, because **that is itself the signal**: the child has not made contact. Run
|
|
286
|
+
`ListAgents` — and note a session started seconds ago can be missing from one listing, so a single
|
|
287
|
+
empty result is not proof it does not exist.
|
|
288
|
+
|
|
245
289
|
Record each step, and the parent may not finish until the child has acknowledged:
|
|
246
290
|
|
|
247
291
|
```bash
|
|
@@ -39,10 +39,10 @@ Find the renderer in this install and run it. Try these paths in order and use t
|
|
|
39
39
|
Then:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
python
|
|
43
|
-
python
|
|
44
|
-
python
|
|
45
|
-
python
|
|
42
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_statusline.py" --install # both hosts
|
|
43
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_statusline.py" --install --host claude
|
|
44
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_statusline.py" --install --host agy
|
|
45
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_statusline.py" --install --dry-run # print, write nothing
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
`--install` does four things:
|
|
@@ -20,9 +20,11 @@ python "<resolved-path>" [--days $ARGUMENTS]
|
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
- Default window is **7 days**. If the user passed a number (e.g. `/usage-review 14`), use it.
|
|
23
|
-
- Run from the project root so `.
|
|
23
|
+
- Run from the project root so `.caddis/usage-log.jsonl` is found correctly. (The legacy
|
|
24
|
+
the legacy `.claude/usage-log.jsonl` is still read as a fallback during the migration.)
|
|
24
25
|
- If the script exits with "No sessions found", tell the user and stop — no further steps.
|
|
25
|
-
- The script prints a markdown report to stdout and writes
|
|
26
|
+
- The script prints a markdown report to stdout and writes
|
|
27
|
+
`.caddis/reviews/usage-review.html` (override with `--output-dir`).
|
|
26
28
|
|
|
27
29
|
## Step 2 — present the report
|
|
28
30
|
|
|
@@ -50,21 +52,22 @@ the two `---` delimiters) of the named agent file and change its value. Show a d
|
|
|
50
52
|
After any applies, tell the user which files changed and suggest:
|
|
51
53
|
"Run `/usage-review` again after a few sessions to see the effect."
|
|
52
54
|
|
|
53
|
-
## Step 5 — offer to schedule a
|
|
55
|
+
## Step 5 — do NOT offer to schedule this as a cloud routine
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
**This command cannot run as a cloud routine, and offering one produces a routine that fails.**
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
`/schedule` creates a **cloud** agent. This command reads `.caddis/usage-log.jsonl`, which the
|
|
60
|
+
local `Stop` hook writes on THIS machine and which is **gitignored** — so it is neither on the
|
|
61
|
+
cloud runner's filesystem nor in the clone it makes. The routine would fire on time and find
|
|
62
|
+
nothing.
|
|
58
63
|
|
|
59
|
-
|
|
64
|
+
That also settles a contradiction this file used to carry: the description says "Data stays
|
|
65
|
+
on-machine — no telemetry, no server", and the old Step 5 offered a cloud routine two screens
|
|
66
|
+
later. The description is the correct half.
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
The routine should fire mid-week so findings are fresh for the next work sprint. If the user
|
|
66
|
-
declines, skip silently — the SessionStart nudge in `inject_relay.py` already covers the
|
|
67
|
-
"you haven't reviewed in 7 days" case.
|
|
68
|
+
**The recurring nudge already exists and is local.** Each run updates
|
|
69
|
+
`.caddis/.last-usage-review`, and the SessionStart hook reads it to remind you when a review is
|
|
70
|
+
overdue. Nothing to schedule.
|
|
68
71
|
|
|
69
72
|
## Notes
|
|
70
73
|
|
|
@@ -7,7 +7,7 @@ description: Explain (or apply) the model lanes — claude / claude-glm / cross-
|
|
|
7
7
|
|
|
8
8
|
Explain the model-switching lanes so the user (or you, on their behalf) can move a session onto an
|
|
9
9
|
OSS provider (GLM, DeepSeek, OpenRouter, or a custom endpoint) in one command, with no hardcoded key
|
|
10
|
-
path. Full detail lives in the **Providers & keys** guide (`docs/guide/providers-and-keys.md`) — this
|
|
10
|
+
path. Full detail lives in the **Providers & keys** guide (`docs/guide/providers-and-keys.md` (in the caddis **source** repo — `docs/` is not shipped inside the installed plugin)) — this
|
|
11
11
|
command is the quick-reference + do-it-now version.
|
|
12
12
|
|
|
13
13
|
## The lanes
|
package/bundles/manifest.json
CHANGED
package/package.json
CHANGED