@caddis/cli 0.4.8 → 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.
@@ -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 (7 categories)
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)?
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "caddis",
3
- "version": "1.3.95",
3
+ "version": "1.3.96",
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
  }
@@ -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
- HANDSHAKE_STATES = ("awaiting-answers", "answered", "verdict-sent", "acknowledged")
474
- _EVENT_STATE = {"answers": "answered", "verdict": "verdict-sent", "ack": "acknowledged"}
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]:
@@ -511,7 +520,8 @@ def handshake_open(repo_root: Path, spawn_id: str, transport: str = "auto",
511
520
  chosen, why = transport, "forced with --transport"
512
521
  state = {
513
522
  "id": spawn_id,
514
- "state": HANDSHAKE_STATES[0],
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],
515
525
  "transport": chosen,
516
526
  "transport_reason": why,
517
527
  # HOW TO ADDRESS THE CHILD. `SendMessage` takes the name `ListAgents` shows, and until
@@ -545,10 +555,19 @@ def handshake_record(repo_root: Path, spawn_id: str, event: str,
545
555
  if state is None:
546
556
  return None, f"no handshake for {spawn_id} — run `handshake open --id {spawn_id}` first"
547
557
  target = _EVENT_STATE[event]
548
- if HANDSHAKE_STATES.index(target) <= HANDSHAKE_STATES.index(state["state"]):
549
- return state, (f"already at `{state['state']}`; `{event}` would not move it forward. "
550
- "Refusing rather than rewriting — an out-of-order event means the round "
551
- "is not the one you think it is.")
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.")
552
571
  if peer:
553
572
  state["peer"] = peer
554
573
  state["history"].append({"event": event, "at": int(time.time()),
@@ -562,6 +581,10 @@ def handshake_record(repo_root: Path, spawn_id: str, event: str,
562
581
  def chase_instruction(state: dict) -> str:
563
582
  """What to actually DO when a chase is due — addressed if we can, honest if we cannot."""
564
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.")
565
588
  if state.get("transport") != "message":
566
589
  return ("This handshake is on the paste route, so the chase goes through the human. Say "
567
590
  "so explicitly — otherwise they watch nothing happen and conclude it is broken.")
@@ -581,7 +604,7 @@ def handshake_chase_due(state: dict, now: int | None = None) -> int:
581
604
  pre-read in the handover file — a stronger test than the original set, and free, because the
582
605
  parent is holding the context anyway.
583
606
  """
584
- if state.get("state") != HANDSHAKE_STATES[0]:
607
+ if state.get("state") not in (HANDSHAKE_STATES[0], HANDSHAKE_STATES[1]):
585
608
  return 0
586
609
  now = int(time.time()) if now is None else now
587
610
  return max(0, now - int(state.get("opened_at", now)) - CHASE_AFTER_S)
@@ -708,7 +731,15 @@ def main(argv: list[str] | None = None) -> int:
708
731
  if a.action == "open":
709
732
  st = handshake_open(root, a.id, a.transport, a.peer)
710
733
  print(f"[spawn] handshake {a.id} open — transport `{st['transport']}` "
711
- 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.")
712
743
  if st["transport"] == "paste":
713
744
  print(" The child cannot message you. Print the prompt for the human to carry, "
714
745
  "and say so — otherwise they watch nothing happen and conclude it is "
@@ -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`; wiring: the caddis repo's `.caddis/plans/model-access.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:
@@ -87,6 +87,7 @@ Record the outcome in relay.md's `## Learnings captured` line (below).
87
87
 
88
88
  ## Step 2c — was any of it the harness?
89
89
 
90
+ <!-- shared:harness-friction-question — keep byte-identical across commands; a test pins it -->
90
91
  **One question, asked here because here is where you are already reflecting.** The observations
91
92
  that reach caddis today are the ones someone happened to recall days later — so small, frequent
92
93
  friction never arrives, which is exactly the friction worth fixing.
@@ -99,6 +100,7 @@ harness, not this repo.
99
100
  ```
100
101
 
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 -->
102
104
 
103
105
  ## Step 3 — write the resume doc (overwrite) with exactly these sections
104
106
  > **Where to write:** solo / single active branch → `.caddis/relay.md` (default).
@@ -46,6 +46,7 @@ surface the `[DOC-MAP]` "read the index first" pointer for future sessions in th
46
46
 
47
47
  ## Step 5 — was any of it the harness?
48
48
 
49
+ <!-- shared:harness-friction-question — keep byte-identical across commands; a test pins it -->
49
50
  **One question, asked here because here is where you are already reflecting.** The observations
50
51
  that reach caddis today are the ones someone happened to recall days later — so small, frequent
51
52
  friction never arrives, which is exactly the friction worth fixing.
@@ -58,6 +59,7 @@ harness, not this repo.
58
59
  ```
59
60
 
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 -->
61
63
 
62
64
  ## KB note format (OKF-lite — mandatory for every new note)
63
65
 
@@ -83,7 +85,7 @@ generated: { by: caddis/<model-id>, at: 2026-07-28T09:00:00Z }
83
85
 
84
86
  The full schema, the caddis↔OKF `status` mapping, and what caddis deliberately skips from OKF v0.2
85
87
  (`sources:`, `Attested Computation`) live in
86
- [`.github/instructions/document-frontmatter.instructions.md`](../../.github/instructions/document-frontmatter.instructions.md).
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.)
87
89
 
88
90
  **Backward compatible:** an existing note with no trust fields is still correct. Do not bulk-migrate
89
91
  old notes — add the fields as notes are next touched.
@@ -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
 
@@ -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 `.claude/usage-log.jsonl` is found correctly.
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 `.claude/usage-review.html`.
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 recurring review (optional)
55
+ ## Step 5 — do NOT offer to schedule this as a cloud routine
54
56
 
55
- After reporting the outcome, ask once:
57
+ **This command cannot run as a cloud routine, and offering one produces a routine that fails.**
56
58
 
57
- > "Want me to schedule this to run automatically every Wednesday? I can set it up with `/schedule`."
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
- If the user says yes, use the `schedule` skill to create a weekly routine:
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
- Schedule a recurring cloud routine: every Wednesday, run /usage-review
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
@@ -1,7 +1,7 @@
1
1
  {
2
- "poolVersion": "1.3.95",
2
+ "poolVersion": "1.3.96",
3
3
  "bundles": {
4
- "antigravity-plugin": "1.3.95",
4
+ "antigravity-plugin": "1.3.96",
5
5
  "antigravity-plugin-extras": "1.3.28",
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.8",
3
+ "version": "0.4.9",
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",