@caddis/cli 0.4.53 → 0.4.55

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.
Files changed (32) hide show
  1. package/bundles/antigravity-plugin/guard_agy.py +184 -0
  2. package/bundles/antigravity-plugin/plugin.json +1 -1
  3. package/bundles/antigravity-plugin/scripts/caddis_budget.py +283 -0
  4. package/bundles/antigravity-plugin/scripts/caddis_checkout_guard.py +5 -1
  5. package/bundles/antigravity-plugin/scripts/caddis_fleet.py +439 -0
  6. package/bundles/antigravity-plugin/scripts/caddis_gate.py +29 -0
  7. package/bundles/antigravity-plugin/scripts/caddis_lanes.py +127 -10
  8. package/bundles/antigravity-plugin/scripts/caddis_notify.py +5 -1
  9. package/bundles/antigravity-plugin/scripts/caddis_roadmap.py +56 -3
  10. package/bundles/antigravity-plugin/scripts/caddis_roadmap_page.py +676 -0
  11. package/bundles/antigravity-plugin/scripts/caddis_roadmap_run.py +39 -6
  12. package/bundles/antigravity-plugin/scripts/caddis_routing.py +4 -4
  13. package/bundles/antigravity-plugin/skills/_registry.md +2 -0
  14. package/bundles/antigravity-plugin/skills/catchup/SKILL.md +4 -0
  15. package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +6 -0
  16. package/bundles/antigravity-plugin/skills/fleet-close/SKILL.md +167 -0
  17. package/bundles/antigravity-plugin/skills/fleet-open/SKILL.md +110 -0
  18. package/bundles/antigravity-plugin/skills/gh-cli/SKILL.md +2 -1
  19. package/bundles/antigravity-plugin/skills/handoff/SKILL.md +101 -1
  20. package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +3 -2
  21. package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +20 -6
  22. package/bundles/antigravity-plugin/skills/refactoring/SKILL.md +2 -1
  23. package/bundles/antigravity-plugin/skills/review-pr/SKILL.md +1 -1
  24. package/bundles/antigravity-plugin/skills/roadmap/SKILL.md +1 -0
  25. package/bundles/antigravity-plugin/skills/ship/SKILL.md +2 -2
  26. package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +9 -5
  27. package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +3 -2
  28. package/bundles/codex/.codex/skills/coding/refactoring/SKILL.md +2 -1
  29. package/bundles/codex/.codex/skills/devops/gh-cli/SKILL.md +2 -1
  30. package/bundles/codex/.codex/skills/workflow/roadmap/SKILL.md +1 -0
  31. package/bundles/manifest.json +2 -2
  32. package/package.json +1 -1
@@ -144,7 +144,10 @@ ENGINES: dict[str, dict] = {
144
144
  PROMPT_ON_STDIN = {"claude"}
145
145
  STUCK_SESSIONS = 3
146
146
  PLAN_PROMPT_NAME = "plan-resume-prompt.md"
147
- HEADLESS_ENV = {"CADDIS_HEADLESS": "1"}
147
+ # CLAUDE_CODE_FORK_SUBAGENT=0: since 2.1.302 fork subagents are on by default in `claude -p`. A fork can run in the
148
+ # background, and a print-mode session can end before it finishes, so its work is lost; the session prompt already
149
+ # asks for foreground work only. Normal subagents stay available.
150
+ HEADLESS_ENV = {"CADDIS_HEADLESS": "1", "CLAUDE_CODE_FORK_SUBAGENT": "0"}
148
151
  ROADMAP_WRITE_TIMEOUT_S = caddis_roadmap.MERGE_CHAIN_TIMEOUT + 600 # a backstop: every call inside is bounded
149
152
  RUNNING = "running" # the last-stop record while a runner holds the lock; a killed run leaves it (needs the owner)
150
153
  GH_STOP_PREFIX = "BLOCKED gh cannot see" # a gh error at a restart: waiting.json stays, a later tick may resume
@@ -192,7 +195,11 @@ def build_command(engine: str, model: str, prompt: str, *, usd: float, timeout_m
192
195
  raise ValueError(f"unknown engine {engine!r}; known: {', '.join(ENGINES)}")
193
196
  exe = list(engine_cmd) if engine_cmd else [engine]
194
197
  if engine == "claude":
195
- tail = [*exe, "-p", "--model", model, "--output-format", "json", "--max-budget-usd", _usd(usd)]
198
+ # The mode is explicit: since Claude Code 2.1.302 a headless run with no mode starts in `default` (third-party
199
+ # providers) and no longer in auto mode, so a machine without `defaultMode` in its settings would stop at
200
+ # every permission prompt. The session runs in its own worktree under the governor, and the guard still runs.
201
+ tail = [*exe, "-p", "--permission-mode", "bypassPermissions", "--model", model, "--output-format", "json",
202
+ "--max-budget-usd", _usd(usd)]
196
203
  elif engine == "agy": # probe 5: --model must come before --print
197
204
  tail = [*exe, "--model", model, "--print", prompt, "--output-format", "json",
198
205
  "--print-timeout", f"{max(1, int(timeout_min))}m"]
@@ -481,6 +488,11 @@ def _create_with(path: Path, text: str) -> None:
481
488
  os.link(tmp, path)
482
489
  except FileExistsError:
483
490
  raise
491
+ except PermissionError as exc:
492
+ if not caddis_roadmap._WINDOWS:
493
+ raise
494
+ # Windows refuses while the old lock's delete is still pending: the name is taken, for now.
495
+ raise FileExistsError(str(path)) from exc
484
496
  except OSError: # no hard links on this file system: O_EXCL, then the text
485
497
  fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
486
498
  with os.fdopen(fd, "w", encoding="utf-8") as fh:
@@ -1187,13 +1199,33 @@ def run(*, repo: Path, plan: str | None = None, roadmap: str | None = None, caps
1187
1199
  if value:
1188
1200
  ctx.masks.append(value)
1189
1201
  if roadmap is not None:
1190
- return _run_roadmap(ctx, template, dry_run, after_session, list(roadmap_cmd or default_roadmap_cmd()),
1191
- hostname or socket.gethostname(), publish or default_publish,
1192
- pr_merged or default_pr_merged, worktree_root,
1193
- delete_remote_branch or default_delete_remote_branch, pr_closed or default_pr_closed)
1202
+ rm_cmd = list(roadmap_cmd or default_roadmap_cmd())
1203
+ try:
1204
+ return _run_roadmap(ctx, template, dry_run, after_session, rm_cmd,
1205
+ hostname or socket.gethostname(), publish or default_publish,
1206
+ pr_merged or default_pr_merged, worktree_root,
1207
+ delete_remote_branch or default_delete_remote_branch, pr_closed or default_pr_closed)
1208
+ finally:
1209
+ if not dry_run:
1210
+ _render_page(ctx, rm_cmd) # the page shows where the run stopped and what waits for the owner
1194
1211
  return _run_plan(ctx, template, dry_run, after_session)
1195
1212
 
1196
1213
 
1214
+ PAGE_RENDER_TIMEOUT_S = 120
1215
+
1216
+
1217
+ def _render_page(ctx: "_Ctx", roadmap_cmd: Sequence[str]) -> None:
1218
+ """Re-render the local roadmap page (roadmap-page Phase 3). Best effort: a failure is one stderr line, never a stop."""
1219
+ argv = [*roadmap_cmd, "render", "--roadmap", ctx.target.slug, "--root", str(ctx.repo)]
1220
+ try:
1221
+ proc = subprocess.run(argv, cwd=ctx.repo, env=headless_env(), stdin=subprocess.DEVNULL, capture_output=True,
1222
+ text=True, encoding="utf-8", errors="replace", timeout=PAGE_RENDER_TIMEOUT_S)
1223
+ if proc.returncode != 0:
1224
+ print(f"caddis_roadmap_run: page not rendered (exit {proc.returncode})", file=sys.stderr)
1225
+ except Exception as exc: # a page must never change how a run ends
1226
+ print(f"caddis_roadmap_run: page not rendered: {type(exc).__name__}", file=sys.stderr)
1227
+
1228
+
1197
1229
  def _dry_run_tail(ctx: _Ctx, template: Path, prompt: str, cwd: Path | None = None) -> None:
1198
1230
  caps = ctx.caps
1199
1231
  cmd = build_command(ctx.engine, ctx.model, "<resume prompt>", usd=min(caps.max_usd_per_session, caps.max_usd_total),
@@ -2042,6 +2074,7 @@ def _run_roadmap(ctx: _Ctx, template: Path, dry_run: bool, after_session: Callab
2042
2074
  finally:
2043
2075
  s.record.setdefault("status", "FAILED")
2044
2076
  ctx.log(s.record)
2077
+ _render_page(ctx, roadmap_cmd)
2045
2078
  if after_session is not None:
2046
2079
  after_session(sessions)
2047
2080
  except _Stop as stop:
@@ -16,14 +16,14 @@ import oss_model
16
16
 
17
17
  # agy is Flash-only on some machines and is good only at small, exact, mechanical cards (112 of 114 passed
18
18
  # in the 2026-10-08 UI-upgrade run) and CI watching. Give design-fidelity, cross-file refactor and judgment
19
- # work to codex or claude. See .caddis/kb/agy-headless-implementer-limits.md. A separate `design` role is an
19
+ # work to claude. Codex left the routing on 2026-10-12 (owner); a config may still name it. See .caddis/kb/agy-headless-implementer-limits.md. A separate `design` role is an
20
20
  # owner decision: it changes the routing line every session prints.
21
21
  DEFAULT_ROUTING: dict[str, list[str]] = {
22
22
  "critical": ["opus"],
23
23
  "light": ["sonnet"],
24
- "coding": ["agy", "codex"],
25
- "review": ["glm", "deepseek"],
26
- "consult": ["agy", "glm", "codex"],
24
+ "coding": ["agy"],
25
+ "review": ["deepseek", "glm"], # owner order 2026-10-16; an agy review runs through caddis_spend.py
26
+ "consult": ["agy", "glm"],
27
27
  "ci-watch": ["agy"],
28
28
  "mechanical": ["haiku"],
29
29
  }
@@ -84,6 +84,8 @@
84
84
  | Digress | `digress/` | Park the current workstream on the stack and switch to a new task — without losing the original |
85
85
  | Excalidraw Db | `excalidraw-db/` | Turn a SQL artifact into an Excalidraw diagram for a design review / ARB pack / slide — higher-level, drag-the-boxes format |
86
86
  | Feature Plan | `feature-plan/` | Create a phased, TDD-structured implementation plan that acts as the durable spine for multi-session work |
87
+ | Fleet Close | `fleet-close/` | Close a fleet of sessions — every session writes its handoff and acks, then print one restart sheet for the owner |
88
+ | Fleet Open | `fleet-open/` | Restore one role from the fleet restart sheet in a new session — name check, relay, tasks, peer greeting, watchers |
87
89
  | Gate Preflight | `gate-preflight/` | Deterministic headless preflight — validates a plan against the actual codebase, self-contained (never depends on a skill activating) |
88
90
  | Gate Review | `gate-review/` | Deterministic headless code review — adversarial diff review, self-contained (never depends on a skill activating) |
89
91
  | Glossary | `glossary/` | Define the words this project invented, in one place — .caddis/kb/GLOSSARY.md |
@@ -46,6 +46,10 @@ Keep it short enough to read at a glance. Roughly:
46
46
  - **The next step**, if one is recorded. Say which file it came from.
47
47
  - **Uncommitted work**, if `git status` is dirty — this is the thing most likely to be lost, and
48
48
  the state file does not track it. Check it; do not assume clean.
49
+ - **The roadmap page**, when the repo has a roadmap (`.caddis/roadmaps/`): re-render it with
50
+ `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_roadmap.py" render --all` (best effort) and give the path it prints
51
+ (`~/.caddis/roadmap-pages/<repo>/<slug>.html`); it shows every session waiting for the owner. The hub of every
52
+ repo on this machine is `~/.caddis/roadmap-pages/index.html`.
49
53
 
50
54
  Leave out anything already done unless the user asks. "What is finished" is a different question
51
55
  from "where are we", and the completed list is usually the longest part of both files.
@@ -54,6 +54,12 @@ points), read it first. It's a free, token-zero project fingerprint that anchors
54
54
  exists instead of assumed. If the work fits comfortably in one session, say so and offer to just do it
55
55
  instead of planning. Only produce a plan for genuinely multi-phase work.
56
56
 
57
+ **Check upstream first when the plan builds on Claude Code behaviour** (hooks, headless `claude -p`, permission
58
+ modes, subagents, worktrees, plugins and skills, Remote Control, background sessions). Read
59
+ `.caddis/kb/claude-code-upstream-watch.md` if the repo has it, and the latest Claude Code changelog for that area
60
+ (Improvements, Bug fixes, In development). Do not plan a fix or a workaround for something Claude Code has already
61
+ changed, fixed or announced; cite the release in **Constraints & decisions** instead.
62
+
57
63
  ### Evidence gate — do not write plan content while a BLOCKER is unresolved
58
64
  This is the direct counter to the failure
59
65
  mode that makes a plan dangerous: **you write the spec, so you cannot see its gaps.** A phase built
@@ -0,0 +1,167 @@
1
+ ---
2
+ name: fleet-close
3
+ description: Close a fleet of sessions — every session writes its handoff and acks, then print one restart sheet for the owner
4
+ ---
5
+
6
+ # /caddis:fleet-close — close every session, collect acks, print the restart sheet
7
+
8
+ Use this when **several** sessions must stop together (a machine restart, a long break, a context
9
+ reset for the whole fleet). For one session, run `/caddis:handoff` instead. This session is the
10
+ **coordinator**: it asks every other session to write its own handoff, collects one ack from each,
11
+ writes its own handoff last, and prints a restart sheet. The owner starts each new session from that
12
+ sheet, and each new session runs `/caddis:fleet-open <role>`.
13
+
14
+ Every fleet rule (names, ack format, order, checks) lives in
15
+ `${CLAUDE_PLUGIN_ROOT}/scripts/caddis_fleet.py`; this command only calls it. Run the steps in order.
16
+
17
+ **Safety rules for the whole command** (a wrong close loses another session's relay or tasks):
18
+ - Never write, edit or move another session's relay, tasks file or watcher files. Each session writes
19
+ its own; you only collect acks.
20
+ - Never stop, end or kill another session, and never tell one to exit. The owner closes terminals
21
+ after reading the sheet.
22
+ - Never wait with a foreground `sleep`. Every wait is a background timer (`run_in_background`).
23
+ - A session that does not ack is never guessed: its row says `no ack`, and the owner runs
24
+ `/caddis:handoff` in it by hand.
25
+
26
+ The ack format (one line in a fenced block, then the session's full resume prompt):
27
+
28
+ ```
29
+ FLEET-ACK name=<n> role=<r> machine=<m> folder=<abs> relay=<path> tasks=<path|none> mode=<new|keeps-running|restore-on:<machine>|nothing> owns-watchers=<yes|no> worktrees=<abs;abs|none>
30
+ ```
31
+
32
+ ## Step 1 — list the fleet
33
+
34
+ Run `ListAgents`. Your own name is in its header line (`This session is <name>`): you are the
35
+ coordinator. Candidates:
36
+ - every local interactive or background session, and every Remote Control session that is not
37
+ `offline`: each gets the close message in Step 3;
38
+ - every cloud session: it is listed as `keeps-running` and gets **no** message (it keeps running in
39
+ the cloud). In Step 4 you save its ack yourself:
40
+ `FLEET-ACK name=<n> role=<role-of n> machine=cloud folder=- relay=- tasks=none mode=keeps-running owns-watchers=no worktrees=none`,
41
+ then the prompt line `Cloud session: keeps running; nothing to restore.`;
42
+ - `offline` rows and your own row are not candidates.
43
+
44
+ For each candidate, and for yourself, get the role:
45
+ `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_fleet.py" role-of <name>`. **Two sessions with the same role**
46
+ would get one start line and one new name, and one of them would be lost. So when roles repeat, keep the
47
+ role for the first name (sorted), and give each other one a distinct role `<role>-b`, `<role>-c`, ...; tell
48
+ that session its role in Step 3 (rule 5).
49
+
50
+ Print the candidate list (name, kind, machine, role) before you send anything. If there are no candidates,
51
+ say so, run `/caddis:handoff` for this session only, and **stop**.
52
+
53
+ ## Step 2 — make the fleet dir
54
+
55
+ The fleet dir is `.caddis/relay/fleet/<MMDD>-<HH>/` in this repo, at the coordinator's local time:
56
+
57
+ ```
58
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_fleet.py" new-name fleet
59
+ ```
60
+
61
+ prints `fleet-<MMDD>-<HH>`; the dir name is the `<MMDD>-<HH>` part. Create it. It is local and
62
+ gitignored (it holds machine paths). Use the same dir for every later step, even if the hour changes.
63
+
64
+ ## Step 3 — send the close message
65
+
66
+ Send each candidate (from Step 1) one `SendMessage`. The first line must be exactly:
67
+
68
+ ```
69
+ FLEET CLOSE from <coordinator>: write your handoff and ack.
70
+ ```
71
+
72
+ Then these close rules, in the message:
73
+ 1. Run `/caddis:handoff` for **your own** relay only. Never touch another session's files. Skip its
74
+ Step 7 (the `Push this note for another machine?` question): the restart sheet carries your prompt,
75
+ and a question nobody answers would stop you before you ack.
76
+ 2. In a shared checkout (its `AGENTS.md` says `Shared checkout: yes`, or `git status` shows changes you
77
+ did not make), skip the steps the handoff checklist skips there.
78
+ 3. If you run watchers: copy each watcher script to `.caddis/relay/<your name>-files/watchers/`, and fill
79
+ the relay's `## Watchers` section (per watcher: script path, exact start command, stop condition,
80
+ the action after the stop).
81
+ 4. Your resume prompt must load your tasks file, greet peers with `<role> is now <your new name>`, and,
82
+ in a shared checkout, say: "read code facts from origin/main; a worktree needs its own fresh
83
+ dependencies".
84
+ 5. Reply to `<coordinator>` with this block (one line, every field filled), then your full resume prompt:
85
+ ```
86
+ FLEET-ACK name=<n> role=<r> machine=<m> folder=<abs> relay=<path> tasks=<path|none> mode=<new|keeps-running|restore-on:<machine>|nothing> owns-watchers=<yes|no> worktrees=<abs;abs|none>
87
+ ```
88
+ Use `name=<your current name>` exactly as `ListAgents` shows it, and `role=<role>` as given in this
89
+ message (a role that repeats in the fleet gets `<role>-b`, ...). `mode=new` if your work continues
90
+ in a new session; `mode=nothing` if it needs no new session (for example a finished read-only review).
91
+ 6. Remote sessions (another machine): also put your open task rows inline after the prompt, as a table
92
+ with exactly this header line (other headers are not read):
93
+ ```
94
+ | Old # | Status | Subject |
95
+ |---|---|---|
96
+ ```
97
+ and say whether your machine restarts (`mode=restore-on:<machine>`) or not (`mode=keeps-running`).
98
+ 7. Do not exit or stop after the ack; the owner closes sessions.
99
+
100
+ ## Step 4 — collect acks (first wait: 10 minutes)
101
+
102
+ Start a background timer, never a foreground wait:
103
+
104
+ ```
105
+ sleep 600
106
+ ```
107
+
108
+ with `run_in_background`. Each ack arrives as a cross-session message. For each one:
109
+ 1. Write the message text (from the `FLEET-ACK` line to the end) to a temp file in your scratchpad.
110
+ 2. Save it:
111
+ ```
112
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_fleet.py" ack add --dir <fleet dir> --text-file <temp file>
113
+ ```
114
+ 3. Exit 0 → acked. An error names the missing or bad field: send that session **one** reply that
115
+ quotes the error and asks for a corrected ack. Do not fix the ack yourself.
116
+ 4. The ack's `name=` must be the name of the candidate that sent it, and its `role=` the role you gave
117
+ it. If not, send **one** reply asking for a corrected ack (a wrong name makes a phantom row and marks
118
+ the real session `no ack`). A second ack from the same session replaces the first (`ack add` saves by
119
+ name).
120
+
121
+ Also save the cloud-session acks from Step 1 here, the same way.
122
+
123
+ ## Step 5 — remind once (second wait: 20 minutes)
124
+
125
+ When the 10-minute timer ends, send each silent candidate **one** reminder:
126
+ `FLEET CLOSE reminder from <coordinator>: your ack is still missing.` Start a 20-minute background
127
+ timer (`sleep 1200`, `run_in_background`) and keep saving acks as in Step 4. When it ends (or every
128
+ candidate has acked), stop waiting. Each session still silent is a `no ack` row (Step 7).
129
+
130
+ **A late ack is never dropped.** Until Step 8 ends, keep saving every ack that arrives, as in Step 4, and
131
+ remove that session from the `--no-ack` list before the next `write`.
132
+
133
+ ## Step 6 — your own handoff and ack
134
+
135
+ Run `/caddis:handoff` for this session (the coordinator's own relay). Then write your own ack (the
136
+ `FLEET-ACK` block with your fields and your resume prompt) to a temp file and save it with
137
+ `ack add`, as in Step 4. The coordinator's ack is never skipped. Then record this session's presence
138
+ (your session id is printed at session start as `[caddis] this session's id: <id>`; never guess one):
139
+
140
+ ```
141
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_fleet.py" whoami --session-id <id> --name <your name> --root .
142
+ ```
143
+
144
+ ## Step 7 — write and check the sheet
145
+
146
+ ```
147
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_fleet.py" write --dir <fleet dir> --coordinator <your name> --no-ack <name> [--no-ack <name> ...]
148
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_fleet.py" check --dir <fleet dir>
149
+ ```
150
+
151
+ Pass `--no-ack <name>` once for each silent session from Step 5, **on every `write`**, and leave it out
152
+ when every session acked (a placeholder would become a fake row) (the no-ack rows are not
153
+ saved between runs). `write` saves `roster.md` and `restart-sheet.md` in the fleet dir.
154
+
155
+ - `check` exit 0 → go to Step 8.
156
+ - `check` exit 1 → send each named session **one** message that quotes its error and asks for a
157
+ corrected ack. Start one 5-minute background timer (`sleep 300`, `run_in_background`) and save the
158
+ corrected acks that arrive. Then run `write` (same `--no-ack` flags) and `check` once more. Errors that
159
+ remain go at the end of `restart-sheet.md` under a heading `## Needs the owner:`, one line each,
160
+ quoted exactly.
161
+ - `write` or `check` exit 3 (no acks, or a file it cannot read) → do not print a sheet. Report the exact
162
+ error, list every ack file in `<fleet dir>/acks/`, and **stop**: the owner decides.
163
+
164
+ ## Step 8 — final reply
165
+
166
+ Reply with the whole text of `restart-sheet.md`, then its absolute path, and nothing else. The
167
+ restart closes this session too, so the sheet must be on screen and on disk.
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: fleet-open
3
+ description: Restore one role from the fleet restart sheet in a new session — name check, relay, tasks, peer greeting, watchers
4
+ ---
5
+
6
+ # /caddis:fleet-open — restore a role from the restart sheet
7
+
8
+ A fleet close (`/caddis:fleet-close`) wrote a restart sheet: one row per role, with the folder to
9
+ start `claude` in, a start line that names the new session, and the role's resume prompt. You are the new session
10
+ for one of those rows. This command restores that role in one go.
11
+
12
+ The argument is the role: `$ARGUMENTS`. If it is empty, list the roles of the newest sheet (Step 1)
13
+ and ask which one — do not guess.
14
+
15
+ Every fleet rule lives in `${CLAUDE_PLUGIN_ROOT}/scripts/caddis_fleet.py`; this command only calls it.
16
+ Run each step in order. A step that says "stop" ends the command: print why, and nothing else.
17
+
18
+ ## Step 1 — find the newest restart sheet that holds the role
19
+
20
+ ```
21
+ ls -d .caddis/relay/fleet/*/restart-sheet.md
22
+ ```
23
+
24
+ The folders are named `<MMDD>-<HH>` (the coordinator's local time). Go from the newest folder to the
25
+ oldest and take the first `restart-sheet.md` with a section `### <n>. <role>`. Read the
26
+ `roster.md` beside it too: its row for the role gives the relay, the tasks file, the folder and the
27
+ mode.
28
+
29
+ - No sheet holds the role → say `No restart sheet holds <role>.` and **stop**.
30
+ - The sheet row says `no new session` (mode `keeps-running`, `nothing` or `no ack`) → say so, quote
31
+ the row's section, and **stop**. A `no ack` row needs `/caddis:handoff` run by hand in the old
32
+ session first.
33
+
34
+ ## Step 2 — check this session's name before loading anything
35
+
36
+ Run `ListAgents`. Its header line says `This session is <name>`; that is your name. The sheet row's
37
+ Start line already names the session: `claude --name <expected> "/caddis:fleet-open <role>"`, where
38
+ `<expected>` is `new_name` of the role at the sheet's hour.
39
+
40
+ - `<name>` equals `<expected>` → go on.
41
+ - Otherwise it must at least be a name of this role:
42
+ ```
43
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_fleet.py" role-of <name>
44
+ ```
45
+ prints `<role>` (the name is `<role>-<MMDD>-<HH>`) → go on, and note the name differs from the sheet.
46
+ - Anything else → tell the owner to run `/rename <expected>` and then `/caddis:fleet-open <role>`
47
+ again, and **stop**. A session cannot rename itself, and a role loaded under the wrong name is a
48
+ role its peers never reach.
49
+
50
+ Nothing is read into this session (no relay, no tasks, no watcher) until this step passes.
51
+
52
+ ## Step 3 — read the relay and run its resume prompt
53
+
54
+ Read the role's relay (the roster's Relay column, `.caddis/relay/<old name>.md`), then follow the
55
+ resume prompt in the sheet section — the fenced block under `### <n>. <role>`. It is the same prompt
56
+ the old session wrote; do what it says, in order.
57
+
58
+ Load the open tasks: every row of the tasks file (roster Tasks file column,
59
+ `.caddis/relay/<old name>.tasks.md`, table `| Old # | Status | Subject |`) becomes one `TaskCreate`
60
+ with the Subject as its subject. A row with Status `in_progress` then gets
61
+ `TaskUpdate` to `in_progress`. A remote role's rows are inline on the sheet under
62
+ `Tasks (inline, remote session):` — load those the same way. Tasks file `none` → 0 tasks.
63
+ Count the tasks you created; Step 6 reports it.
64
+
65
+ ## Step 4 — greet the live local peers
66
+
67
+ From the `ListAgents` output of Step 2 (run it again if more than a few minutes passed), take each
68
+ live local session whose role is in the roster (`caddis_fleet.py role-of <their name>`), other than
69
+ your own role. When a role has several names, address the newest:
70
+
71
+ ```
72
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_fleet.py" newest <their role> --names-file <file with one name per line>
73
+ ```
74
+
75
+ Send each one message, exactly: `<role> is now <name>` (your role, your name). Skip offline sessions
76
+ and cloud sessions. Keep the list of names you greeted for Step 6.
77
+
78
+ ## Step 5 — restart the watchers
79
+
80
+ Read the relay's `## Watchers` section. For each entry:
81
+
82
+ 1. Print the entry as written (script path, start command, stop condition, after-action).
83
+ 2. Check the script path exists (handoff copied it to `.caddis/relay/<old name>-files/watchers/`)
84
+ and that the stop condition and the after-action are both in the relay. If either is missing,
85
+ do not start it: report it as `watcher not started: <reason>` in Step 6.
86
+ 3. Start it with its exact start command through Bash with `run_in_background`. Never a foreground
87
+ `sleep` or loop.
88
+
89
+ No `## Watchers` section → 0 watchers.
90
+
91
+ ## Step 6 — record the name and report
92
+
93
+ Record this session's presence (your session id is printed at session start as
94
+ `[caddis] this session's id: <id>`; never guess one):
95
+
96
+ ```
97
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_fleet.py" whoami --session-id <id> --name <name> --root .
98
+ ```
99
+
100
+ Then print one line, and any `watcher not started:` lines under it:
101
+
102
+ ```
103
+ [FLEET-OPEN] <role> restored: <n> tasks, <k> watchers, peers greeted: <names>
104
+ ```
105
+
106
+ ## Rules
107
+
108
+ - One role per session. To restore another role, start another session from the sheet.
109
+ - Never `claude --resume` the old session: the sheet exists so every role starts fresh.
110
+ - Never edit the restart sheet or the roster; they are the record of the close.
@@ -167,7 +167,8 @@ gh alias set review 'pr list --search "review-requested:@me"'
167
167
 
168
168
  ```bash
169
169
  gh issue develop 42 --branch fix/issue-42
170
- git add . && git commit -m "fix: resolve login bug" && git push -u origin HEAD
170
+ git add src/auth/login.py tests/test_login.py # named files only, never `git add .`
171
+ git commit -m "fix: resolve login bug" && git push -u origin HEAD
171
172
  gh pr create --title "fix: resolve login bug" --body "Closes #42"
172
173
  gh pr checks --watch
173
174
  gh pr merge --squash --delete-branch
@@ -8,6 +8,9 @@ description: End-of-session handoff — capture exact state so the next session
8
8
  You are ending a work session. Produce/refresh this workstream's resume doc (`.caddis/relay/<name>.md`) so the next session
9
9
  (you, a future you, or another agent on any tool) can resume immediately. This is the anti-context-rot checkpoint.
10
10
 
11
+ Several sessions to close together? Run `/caddis:fleet-close` in one of them instead; it asks each session to run this
12
+ command and collects one restart sheet.
13
+
11
14
  ## Step 0 — is this a shared checkout?
12
15
  Another session may have uncommitted edits in this checkout. A handoff that commits here can carry
13
16
  them into its commit, so check first, before Step 1.
@@ -33,9 +36,12 @@ them into its commit, so check first, before Step 1.
33
36
  - Step 3, point 4: run `caddis_rows.py set --file <worktree>/.caddis/open-work.md ...`, and make the
34
37
  plan's Tracker edit in `<worktree>/.caddis/plans/<feature>.md`, not in this checkout.
35
38
  - Step 4: skip `caddis_tidy.py --apply` (report `skipped: shared checkout`).
39
+ - Step 6: skip `caddis_todo.py reconcile` (report `skipped: shared checkout`; it writes tracked
40
+ `.caddis/todo-list/` files and has no worktree option).
36
41
  - Step 6b: skip `session_state.py prune` (report `skipped: shared checkout`).
42
+ - Step 6c: run `--reindex` in the worktree (it edits the tracked `DOC-MAP.md`).
37
43
  - The relay stays in this checkout: `.caddis/relay/` is local and gitignored.
38
- After Step 6b, run:
44
+ After Step 6c, run:
39
45
  ```
40
46
  python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_docs_pr.py" finish --worktree <path> --title "docs(handoff): <name> session notes" --body "Handoff doc edits from a shared checkout."
41
47
  ```
@@ -44,6 +50,30 @@ them into its commit, so check first, before Step 1.
44
50
  it never merges. On a failure it prints `PATCH: <path>` with the edits; report that path. Put the
45
51
  `PR:` line and the file list in your final report, under `Docs PR needed:`.
46
52
 
53
+ ## Checklist
54
+ The handoff is a fixed checklist. Every row ends as `done`, `skipped: <reason>` or `failed: <error>`, and
55
+ Step 8 prints all 13 in this order. Never drop a row from the report, even when its step did not apply.
56
+
57
+ | # | Row | What it is | Shared checkout (Step 0) |
58
+ |---|---|---|---|
59
+ | 1 | `knowledge-transfer` | Step 1 | edits only in the Step 0 worktree |
60
+ | 2 | `plan-tracker` | the plan's Tracker row (Rules) | edit in the Step 0 worktree |
61
+ | 3 | `open-work-row` | Step 3, point 4: `caddis_rows.py set` | `--file <worktree>/...` |
62
+ | 4 | `relay` | Step 3: the relay, written from scratch with current state only | the relay is local; runs |
63
+ | 5 | `tasks-file` | Step 3c | runs |
64
+ | 6 | `kb-index` | Step 6c: refresh the KB index | in the Step 0 worktree |
65
+ | 7 | `violations-report` | Step 6c: report standing violations, each with a one-line fix command | runs (report only) |
66
+ | 8 | `todo-reconcile` | Step 6: `caddis_todo.py reconcile` | `skipped: shared checkout` |
67
+ | 9 | `usage-record` | Step 6b: `session_state.py usage` | runs |
68
+ | 10 | `tidy-apply` | Step 4: `caddis_tidy.py --apply` | `skipped: shared checkout` |
69
+ | 11 | `state-prune` | Step 6b: `session_state.py prune` | `skipped: shared checkout` |
70
+ | 12 | `prune-list` | Step 6b: `caddis_tidy.py --prune`, list only | runs (lists, never applies) |
71
+ | 13 | `handover-check` | Step 5 | runs |
72
+
73
+ `tidy-apply`, `state-prune` and `todo-reconcile` run only on `SHARED: no`. Handoff never runs
74
+ `caddis_tidy.py --prune --apply` on its own, in any checkout; only the user can ask for it (Step 6b). In a
75
+ shared checkout the report also lists every tracked file Step 1 changed, under `Docs PR needed:`.
76
+
47
77
  ## Step 1 — capture learnings FIRST (knowledge-transfer)
48
78
  **Before** gathering git signals or writing relay.md, decide on `knowledge-transfer` — and the default
49
79
  is to run it.
@@ -107,6 +137,9 @@ Nothing to say is the normal answer. Say nothing and move on — this must not b
107
137
  <!-- /shared:harness-friction-question -->
108
138
 
109
139
  ## Step 3 — write the resume doc (overwrite) with exactly these sections
140
+ Write the relay from scratch with the current state only. Never append to the old text: git keeps the
141
+ history, and the relay has a 4,000-character budget (`caddis_budget.py`).
142
+
110
143
  > **Where to write:** this session's own workstream relay, `.caddis/relay/<name>.md`, through the
111
144
  > locked script — never with a plain file edit, which would skip the conflict check. `.caddis/relay.md`
112
145
  > is a generated index of all workstreams; do not write it by hand.
@@ -184,7 +217,9 @@ knowledge-transfer: <✓ ran → files written | ✗ skipped → reason>
184
217
  ## Resume prompt
185
218
  \`\`\`
186
219
  Read .caddis/relay/<name>.md, then the plan it points to. Continue from <phase/step>. Next action: <exact>.
220
+ Load every row of .caddis/relay/<name>.tasks.md with TaskCreate.
187
221
  \`\`\`
222
+ (Keep the `Load every row` sentence only when Step 3c wrote `.caddis/relay/<name>.tasks.md`.)
188
223
 
189
224
  STATUS: <CONTINUE | OWNER-GATE <P>-OG<n> | BLOCKED <owner> | FAILED <reason>>
190
225
  ```
@@ -195,6 +230,20 @@ Small work that does not meet a trigger skips its block.
195
230
 
196
231
  **Trigger: Always.** The relay ends with a status line, the last non-empty line, and it is exactly one of `STATUS: CONTINUE`, `STATUS: OWNER-GATE <P>-OG<n>`, `STATUS: BLOCKED <owner>` or `STATUS: FAILED <reason>`. In Step 3, point 5, write the relay with `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_workstreams.py" put <name> <temp-file> --require-status --expected-hash <hash>` instead of `write` (in short: put --require-status), with the same hash Step 3 read, so a peer's newer relay is never overwritten. It rejects a relay with no valid status line, and renders the index. When the repo has `.caddis/open-work.md` with `type: open-work` in its frontmatter, also set each open-work row's status with `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_rows.py" set --file <open-work.md> --id <row id> --col Status=<status>` (in a shared checkout, the copy in the Step 0 worktree). When the status is `done`, also pass `--col Evidence=<commit, PR or test>`: caddis_rows.py refuses `done` with no Evidence.
197
232
 
233
+ ## Step 3c - Save the open tasks
234
+ The task list dies with the session unless it is saved. Write this session's open tasks beside
235
+ the relay:
236
+ ```
237
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/session_state.py" tasks-file --session-id <session-id> --relay <name> --root .
238
+ ```
239
+ It reads the `## Tasks` table of `.caddis/session-state/<session-id>.md`, keeps the rows that are
240
+ not `done` or `deleted`, and prints `[TASKS] <n> open task(s) -> <path>` or `[TASKS] none open`
241
+ (with no open task it deletes an old `.caddis/relay/<name>.tasks.md`). When it wrote a file, the
242
+ relay's resume prompt must hold the sentence
243
+ `Load every row of .caddis/relay/<name>.tasks.md with TaskCreate.`: Step 5 blocks a relay whose
244
+ tasks file exists and is not named. If you wrote the relay before running this, rewrite it with
245
+ Step 3, point 5.
246
+
198
247
  ## Step 4 — tidy finished artifacts
199
248
  Flip any plan/prompt's frontmatter `status:` to `done` (or `superseded`) if this session actually
200
249
  finished it — that is your judgment call; the script below never makes it and never auto-flips
@@ -260,6 +309,34 @@ It only lists: session-state files older than 7 days, logs over 5 MB to rotate,
260
309
  only, never deleted) orphan `.lock` files and lane worktrees older than 7 days. Include its report. **Never
261
310
  add `--apply` yourself**; deleting is the user's choice. If they ask, run `--prune --apply`.
262
311
 
312
+ ## Step 6c — KB index and standing violations
313
+ Both scripts fall back to `claude-harness/scripts/check_doc_coverage.py` and `scripts/caddis_tidy.py` in a
314
+ source checkout; a script that is missing is `skipped: <script> not installed`.
315
+
316
+ Checklist row 6, `kb-index` (in a shared checkout, run it in the Step 0 worktree, before the Step 0
317
+ `finish`; it edits `DOC-MAP.md`):
318
+ ```
319
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/check_doc_coverage.py" --reindex
320
+ ```
321
+ Checklist row 7, `violations-report` (all three only report; never fix files here):
322
+ ```
323
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_tidy.py" --check
324
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/check_doc_coverage.py"
325
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_budget.py" check --root . --report
326
+ ```
327
+ `caddis_budget.py` lists each handoff-grown file over its size budget (`OVER <path>: ...; fix: <rule>`); the
328
+ `fix:` text is the one-line fix for that row.
329
+ List each violation they print with a one-line command that fixes it (for example `/caddis:kb` for an
330
+ un-indexed note, or `caddis_tidy.py --apply` for a finished artifact in the wrong folder). A violation is
331
+ not a failure of the handoff: the row is `done` when all three ran. If a script is missing, the row is
332
+ `skipped: <script> not installed`.
333
+
334
+ Then, when the repo has a roadmap (`.caddis/roadmaps/`), re-render its page so phases built by hand show too
335
+ (best effort, not a checklist row; it writes only under `~/.caddis/roadmap-pages/`, never in this checkout):
336
+ ```
337
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_roadmap.py" render --all
338
+ ```
339
+
263
340
  ## Step 7 — push prompt (interactive only)
264
341
  If this is an interactive session, ask the user every time — never reuse an earlier answer:
265
342
  `Push this note for another machine? (y/N)`
@@ -272,6 +349,29 @@ index or staged files — after the secret filter passes. Report its one-line re
272
349
  `y` means do nothing. Headless runs never ask and never push (the script also refuses headless).
273
350
  On the other machine, `/catchup <name>` fetches it.
274
351
 
352
+ ## Step 8 — the checklist report
353
+ End the handoff with this block, every row filled with `done`, `skipped: <reason>` or `failed: <error>`:
354
+
355
+ ```
356
+ === HANDOFF CHECKLIST ===
357
+ 1 knowledge-transfer done | skipped: <reason> | failed: <error>
358
+ 2 plan-tracker <same three forms>
359
+ 3 open-work-row <same three forms>
360
+ 4 relay <same three forms>
361
+ 5 tasks-file <same three forms>
362
+ 6 kb-index <same three forms>
363
+ 7 violations-report <same three forms>
364
+ 8 todo-reconcile <same three forms>
365
+ 9 usage-record <same three forms>
366
+ 10 tidy-apply <same three forms>
367
+ 11 state-prune <same three forms>
368
+ 12 prune-list <same three forms>
369
+ 13 handover-check <same three forms>
370
+ Shared checkout: yes|no <reason>
371
+ ```
372
+
373
+ Then, in a shared checkout, `Docs PR needed:` with the `PR:` line and the file list from Step 0.
374
+
275
375
  ## Rules
276
376
  - **One next action, by priority ladder.** `## Next step` names exactly ONE action — not a menu. Pick it in
277
377
  order: (1) interrupted mid-phase work → finish + commit it; else (2) the active plan's next not-started
@@ -24,8 +24,9 @@ It ships with the harness. Try, in order:
24
24
  git -C <target> status --short
25
25
  ```
26
26
  The migration **refuses a dirty tree** (a rename mixed into unrelated edits is unreviewable, and
27
- these working trees are shared with parallel sessions). If it's dirty: commit or stash first. Never
28
- pass `--allow-dirty` on a tree whose changes you didn't make.
27
+ these working trees are shared with parallel sessions). If it's dirty with your own changes: commit
28
+ them first. If it holds changes you did not make, stop and ask the owner: never commit, stash or reset
29
+ them, and never pass `--allow-dirty` on such a tree.
29
30
 
30
31
  ## Step 3 — dry run
31
32
  ```