@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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "caddis",
3
- "version": "1.3.99",
3
+ "version": "1.3.101",
4
4
  "description": "caddis — agent-agnostic dev harness as an Antigravity (agy) plugin: TDD skills + commands, single-sourced with the Claude Code plugin."
5
5
  }
@@ -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. Never rewinds: a verdict cannot un-answer the questions, and an
552
- out-of-order event is a sign the operator has lost track of which round they are in — which
553
- is exactly when a silent overwrite would hurt."""
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
- print(f"[spawn] {a.id}: {st['state']}")
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
 
@@ -1,7 +1,7 @@
1
1
  {
2
- "poolVersion": "1.3.99",
2
+ "poolVersion": "1.3.101",
3
3
  "bundles": {
4
- "antigravity-plugin": "1.3.99",
4
+ "antigravity-plugin": "1.3.101",
5
5
  "antigravity-plugin-extras": "1.3.29",
6
6
  "codex": "unknown"
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caddis/cli",
3
- "version": "0.4.11",
3
+ "version": "0.4.12",
4
4
  "description": "Install, update and diagnose the caddis dev harness across every coding agent you use (Claude Code, agy/Antigravity, and more).",
5
5
  "keywords": [
6
6
  "caddis",