@caddis/cli 0.4.11 → 0.4.12
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_spawn.py +60 -6
- package/bundles/antigravity-plugin/scripts/setup_project_ai.py +10 -0
- package/bundles/antigravity-plugin/skills/spawn-session/SKILL.md +15 -0
- package/bundles/antigravity-plugin/skills/ui-brief/SKILL.md +6 -1
- package/bundles/manifest.json +2 -2
- package/package.json +1 -1
|
@@ -546,14 +546,58 @@ def handshake_read(repo_root: Path, spawn_id: str) -> dict | None:
|
|
|
546
546
|
return None
|
|
547
547
|
|
|
548
548
|
|
|
549
|
+
MAX_REJECTS = 2
|
|
550
|
+
|
|
551
|
+
|
|
549
552
|
def handshake_record(repo_root: Path, spawn_id: str, event: str,
|
|
550
|
-
peer: str = "") -> tuple[dict | None, str]:
|
|
551
|
-
"""Advance the handshake.
|
|
552
|
-
|
|
553
|
-
is
|
|
553
|
+
peer: str = "", verdict: str = "") -> tuple[dict | None, str]:
|
|
554
|
+
"""Advance the handshake. One step forward, with exactly one named exception.
|
|
555
|
+
|
|
556
|
+
**REJECT sends it back to `awaiting-answers`, and that is the whole point of this function
|
|
557
|
+
having a rewind at all.** `spawn-session.md` defines REJECT as "an instruction to re-read named
|
|
558
|
+
sections and re-answer", and says "two REJECTs on one spawn means the HANDOVER is at fault —
|
|
559
|
+
regenerate it". Neither was possible until 2026-09-10: the states were strictly linear, so
|
|
560
|
+
after `verdict-sent` the only legal event was `ack`, and the handshake did not even record
|
|
561
|
+
WHICH verdict was sent. ACCEPT and REJECT were indistinguishable in the state file, so nothing
|
|
562
|
+
could count REJECTs and the two-strike rule was unenforceable prose.
|
|
563
|
+
|
|
564
|
+
Every other rewind is still refused. An out-of-order event is an operator who has lost track of
|
|
565
|
+
the round, which is exactly when a silent overwrite would hurt.
|
|
566
|
+
"""
|
|
554
567
|
state = handshake_read(repo_root, spawn_id)
|
|
555
568
|
if state is None:
|
|
556
569
|
return None, f"no handshake for {spawn_id} — run `handshake open --id {spawn_id}` first"
|
|
570
|
+
|
|
571
|
+
verdict = (verdict or "").strip().upper()
|
|
572
|
+
if event == "verdict" and verdict == "REJECT":
|
|
573
|
+
if state["state"] != "answered":
|
|
574
|
+
return state, (f"at `{state['state']}` — a REJECT only follows `answered`. "
|
|
575
|
+
"There is nothing to reject yet.")
|
|
576
|
+
n = int(state.get("rejects", 0)) + 1
|
|
577
|
+
if n > MAX_REJECTS - 1:
|
|
578
|
+
state["rejects"] = n
|
|
579
|
+
# Recorded even though it is refused. The counter is the enforceable fact, but a
|
|
580
|
+
# history showing one REJECT beside a count of two is a state file that contradicts
|
|
581
|
+
# itself, and the next reader has to work out which half to believe.
|
|
582
|
+
state["history"].append({"event": "verdict", "verdict": "REJECT", "refused": True,
|
|
583
|
+
"at": int(time.time()),
|
|
584
|
+
**({"peer": peer} if peer else {})})
|
|
585
|
+
_handshake_path(repo_root, spawn_id).write_text(
|
|
586
|
+
json.dumps(state, indent=2) + chr(10), encoding="utf-8")
|
|
587
|
+
return state, (
|
|
588
|
+
f"REJECT #{n}. `spawn-session.md`: two REJECTs on one spawn means the HANDOVER is "
|
|
589
|
+
"at fault, not the successor. Regenerate it — do not coach the child through a "
|
|
590
|
+
"third round. Close this handshake and open a new one on a rewritten handover.")
|
|
591
|
+
state["rejects"] = n
|
|
592
|
+
state["state"] = "awaiting-answers"
|
|
593
|
+
state["history"].append({"event": "verdict", "verdict": "REJECT", "at": int(time.time()),
|
|
594
|
+
**({"peer": peer} if peer else {})})
|
|
595
|
+
if peer:
|
|
596
|
+
state["peer"] = peer
|
|
597
|
+
_handshake_path(repo_root, spawn_id).write_text(
|
|
598
|
+
json.dumps(state, indent=2) + chr(10), encoding="utf-8")
|
|
599
|
+
return state, ""
|
|
600
|
+
|
|
557
601
|
target = _EVENT_STATE[event]
|
|
558
602
|
here = HANDSHAKE_STATES.index(state["state"])
|
|
559
603
|
there = HANDSHAKE_STATES.index(target)
|
|
@@ -571,6 +615,7 @@ def handshake_record(repo_root: Path, spawn_id: str, event: str,
|
|
|
571
615
|
if peer:
|
|
572
616
|
state["peer"] = peer
|
|
573
617
|
state["history"].append({"event": event, "at": int(time.time()),
|
|
618
|
+
**({"verdict": verdict} if event == "verdict" and verdict else {}),
|
|
574
619
|
**({"peer": peer} if peer else {})})
|
|
575
620
|
state["state"] = target
|
|
576
621
|
_handshake_path(repo_root, spawn_id).write_text(
|
|
@@ -659,6 +704,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
659
704
|
h.add_argument("--id", required=True)
|
|
660
705
|
h.add_argument("--repo-root", default=".")
|
|
661
706
|
h.add_argument("--event", choices=sorted(_EVENT_STATE))
|
|
707
|
+
h.add_argument("--verdict", default="",
|
|
708
|
+
choices=["", "ACCEPT", "ACCEPT-WITH-CORRECTION", "REJECT"],
|
|
709
|
+
help="with `--event verdict`. REJECT returns the handshake to "
|
|
710
|
+
"`awaiting-answers` so the child re-reads and re-answers, and counts the "
|
|
711
|
+
"strike; the second REJECT refuses and tells you to regenerate the "
|
|
712
|
+
"handover instead of coaching the child through a third round")
|
|
662
713
|
h.add_argument("--transport", choices=["auto", "message", "paste"], default="auto")
|
|
663
714
|
h.add_argument("--peer", default="",
|
|
664
715
|
help="the child's name as ListAgents shows it — take it from the `from-name` "
|
|
@@ -763,11 +814,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
763
814
|
if not a.event:
|
|
764
815
|
sys.stderr.write("[spawn] --event is required for `record`" + chr(10))
|
|
765
816
|
return EXIT_REFUSED
|
|
766
|
-
st, err = handshake_record(root, a.id, a.event, a.peer)
|
|
817
|
+
st, err = handshake_record(root, a.id, a.event, a.peer, a.verdict)
|
|
767
818
|
if err:
|
|
768
819
|
sys.stderr.write("[spawn] " + err + chr(10))
|
|
769
820
|
return EXIT_REFUSED
|
|
770
|
-
|
|
821
|
+
rejects = int((st or {}).get("rejects", 0))
|
|
822
|
+
tail = f" (REJECT #{rejects} — re-read and re-answer)" if \
|
|
823
|
+
(a.verdict.upper() == "REJECT" and rejects) else ""
|
|
824
|
+
print(f"[spawn] {a.id}: {st['state']}{tail}")
|
|
771
825
|
return EXIT_OK
|
|
772
826
|
if a.action == "close" and not (st.get("peer") or "").strip() \
|
|
773
827
|
and st["state"] != HANDSHAKE_STATES[0]:
|
|
@@ -699,6 +699,16 @@ repo**: an access request, a decision from another team, a change only someone e
|
|
|
699
699
|
table stops a sent message from going unchased. Comms are almost always sent to get an *action*, and
|
|
700
700
|
an unanswered ask looks exactly like a forgotten one unless something is tracking it.
|
|
701
701
|
|
|
702
|
+
**Read that rule the other way round: THIS TABLE IS WHERE YOU LOOK BEFORE YOU WRITE ONE.** Check it
|
|
703
|
+
before drafting, not only after filing. A handover transfers a task, and for the whole overlap
|
|
704
|
+
window two sessions can both believe an outbound note is theirs - on 2026-09-08 two of them wrote
|
|
705
|
+
the same note five minutes apart, and both sat here marked ready to send. Two notes on one subject
|
|
706
|
+
reaching one team reads as one team not talking to itself.
|
|
707
|
+
|
|
708
|
+
**If you do find a duplicate, do not delete it.** Verify its unique contributions independently,
|
|
709
|
+
fold them into the survivor, and archive the loser with a banner saying why. The second draft is a
|
|
710
|
+
second reading - the one in that incident found a defect in the first.
|
|
711
|
+
|
|
702
712
|
| Raised | Audience | Subject | Status | Blocks | File |
|
|
703
713
|
|---|---|---|---|---|---|
|
|
704
714
|
| _(none yet)_ | | | | | |
|
|
@@ -360,6 +360,21 @@ The vocabulary matches `/caddis:validate-phase` on purpose: same discipline, dif
|
|
|
360
360
|
**Two REJECTs on one spawn means the HANDOVER is at fault, not the successor. Regenerate it** rather
|
|
361
361
|
than coaching the child through it.
|
|
362
362
|
|
|
363
|
+
**Record the verdict — the machine enforces both of those rules only if you tell it which one:**
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_spawn.py" handshake record --id <id> --event verdict --verdict REJECT
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
A REJECT returns the handshake to `awaiting-answers`, so `record --event answers` is legal again
|
|
370
|
+
and the child re-answers. The **second** REJECT is refused: it tells you to regenerate the handover,
|
|
371
|
+
and the only way forward is then ACCEPT or abandoning the handshake.
|
|
372
|
+
|
|
373
|
+
Until 2026-09-10 `--verdict` did not exist. The states were strictly linear, so after `verdict-sent`
|
|
374
|
+
the only legal event was `ack`, and nothing recorded WHICH verdict was sent — ACCEPT and REJECT were
|
|
375
|
+
indistinguishable in the state file. So "re-read and re-answer" was impossible and "two REJECTs
|
|
376
|
+
means regenerate" was unenforceable, because nothing counted them.
|
|
377
|
+
|
|
363
378
|
### When the fault is yours, fix the class
|
|
364
379
|
|
|
365
380
|
If validation finds an error that came from your handover, **fix the source document in the same
|
|
@@ -74,7 +74,12 @@ Every component must reflect the agreed colour story and font pairing — no fal
|
|
|
74
74
|
|
|
75
75
|
**Motion discipline:**
|
|
76
76
|
- Use animation as communication, not decoration
|
|
77
|
-
- Always respect `prefers-reduced-motion` media query
|
|
77
|
+
- Always respect `prefers-reduced-motion` media query — but treat it as a DEFAULT, not a verdict.
|
|
78
|
+
Windows Server ships with animations off with no user preference expressed, so a build that only
|
|
79
|
+
checks the media query once at load and never offers a way back can freeze a genuinely correct
|
|
80
|
+
animation for the whole session with no override (found in `.caddis/second-brain.html`: the OS
|
|
81
|
+
read is captured into a `var` at script start and never re-read). Read the OS default into a
|
|
82
|
+
visible toggle, and have the draw loop read the toggle's current state, not a captured constant.
|
|
78
83
|
- One hero animation moment — not every element animating on load
|
|
79
84
|
- 44px minimum touch targets on mobile
|
|
80
85
|
|
package/bundles/manifest.json
CHANGED
package/package.json
CHANGED