@caddis/cli 0.4.12 → 0.4.14

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.
@@ -49,7 +49,10 @@ human to carry, which meant the child did not exist yet and its address could no
49
49
  first message was itself proof it had read the handover. Here the parent speaks first, so that
50
50
  proof is a separate step: **your opening message asks the child to reply with WHICH FILES it
51
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`.
52
+ anything. **Ask also how many open tasks it rebuilt** from `.caddis/parent-session-state.md` into
53
+ its own tracker, whatever tracker it has. Record both with `--event readback --revived <n>`. The
54
+ handshake refuses a read-back with no count, and a count that disagrees with the file's `- [ ]`
55
+ lines. Until the read-back is recorded, it refuses `--event answers`.
53
56
 
54
57
  **No name given?** The command falls back to the older printed-prompt flow. That is also the only
55
58
  route on agy and Codex, which have no `SendMessage`.
@@ -67,7 +70,7 @@ and the active plan).
67
70
  |---|---|---|
68
71
  | 1 | **Capture the knowledge** — KB notes for anything that generalises | FIRST, while the reasoning is still in context. Written last it becomes a summary of a summary |
69
72
  | 2 | **Update the durable state** — the register, the parking lot | These are the authority. The relay quotes them, so they must be true before it does |
70
- | 3 | **Write the TASK LIST to a file** | A task widget does not survive a `/clear`. This project has already lost one |
73
+ | 3 | **Write the TASK LIST to a file** — one task per line, `- [ ]` open, `- [x]` done | **Task state is per-session.** The successor's tracker starts empty: a parent board held 59 tasks and the child's `TaskList` said "No tasks found". The child may not even have the same tracker tool. The file is the only medium both sessions can read, so never tell the successor it shares your board |
71
74
  | 4 | **Write the relay** — what happened, who owns what, what is blocked | It CITES 1-3. Written earlier, it cites things that have since moved |
72
75
  | 5 | **Derive the questions** | LAST. They must target what steps 1-4 actually say. Questions written first test **your memory**, which is the thing under suspicion |
73
76
 
@@ -170,9 +173,19 @@ or from a file quoted by path.
170
173
  python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_spawn.py" check --doc .caddis/spawn-session/<id>-prompt.md
171
174
  ```
172
175
 
173
- It **blocks** on a commit hash or test count given as current state, and on any path that does not
174
- exist. It only **notes** a historical hash — those are durable, and flagging them was how the first
175
- version produced seven findings on one document and taught the reader to skim.
176
+ It **blocks** on a commit hash or test count given as current state, and on a path that does not
177
+ exist. It only **notes** three things:
178
+
179
+ - a historical hash — durable;
180
+ - a count quoted from an earlier document — `a previous handover said "1,066 tests"` is a citation,
181
+ not a claim. It needs a reporting verb before the quote and no now-claim on the line; quotation
182
+ marks alone still block;
183
+ - a bare filename that matches no file in this repo — usually a file in another repository.
184
+
185
+ A bare filename that DOES match a file here still blocks, and the refusal names the full path. That
186
+ is the case that caught a real error: `deploy.ps1` named the canonical copy when the task was about
187
+ a different file. Flagging citations was how the first version produced seven findings on one
188
+ document and taught the reader to skim.
176
189
 
177
190
  **A claim that cannot be re-derived does not go in the handover.**
178
191
 
@@ -300,18 +313,54 @@ a handover, it is a file — and grading RE-DERIVES, so it audits the document a
300
313
  reader. In the manual run that pass found a defect in the parent's own handover. `/clear` in your
301
314
  terminal destroys the only thing that can do that.
302
315
 
303
- > **You stay ALIVE. You stop WRITING.** `/caddis:spawn-hub` carries a single-writer rule — the
304
- > outgoing session stops writing to the repository the moment the prompt is issued, because two
305
- > sessions committed to one repo concurrently and one's work landed inside the other's commit.
306
- > That is a different rule from this one and **both hold.** Said no other way, a reader reconciles
307
- > them by closing the parent early, which removes the grader.
316
+ ### Grading goes to a fresh subagent — not to you
317
+
318
+ **This command's trigger is a nearly-full context, and grading is its most expensive step.** Round 1
319
+ re-derives every answer from the repository, because no answer key is stored. In the first real run
320
+ that was about 30 tool calls, and round 2 repeated it for the delivered work. A parent near its limit
321
+ cannot finish that. It runs out mid-grade, while the successor waits for a verdict.
322
+
323
+ Grading never needed your memory. It needs the repo, the questions and the child's answers. So send
324
+ each round to a **fresh subagent** and keep only its verdict:
325
+
326
+ > Grade `.caddis/spawn-session/<id>-answers.md` against the questions in
327
+ > `.caddis/spawn-session/<id>-prompt.md`. Re-derive every answer from the repository — run the
328
+ > commands, open the files — and do not trust the child's citations. Return ACCEPT, ACCEPT WITH
329
+ > CORRECTIONS or REJECT, and for each wrong answer the file or command that proves it. List
330
+ > separately any error you find in the handover documents themselves.
331
+
332
+ Use a read-only agent that can open files and run commands (`caddis:anchor` fits). You relay the
333
+ verdict, record it, and fix any handover document it proves wrong. On a runtime with no subagents,
334
+ grade in a fresh session and say so. Never grade from a nearly-full context.
335
+
336
+ **ALIVE until `handshake close`; not WRITING, with two named exceptions.** `/caddis:spawn-hub`
337
+ carries a single-writer rule, because two sessions committed to one repo concurrently and one's work
338
+ landed inside the other's commit. Here it has exactly two exceptions: your own handover artefacts,
339
+ and a document a verdict proves wrong. **Commit and push each one before you reply.** Never hold an
340
+ uncommitted change while the child is live, because a checkout on its side can take it with no copy
341
+ anywhere.
308
342
 
309
343
  Note the handshake file lands in `.caddis/spawn-session/`, so commit it with the others before
310
344
  re-running `preflight`, which refuses on a dirty tree.
311
345
 
312
- ### Tell the child it does not own the tree
346
+ ### The tree is never handed over while two sessions are attached
313
347
 
314
- **Say this in the prompt.** A fresh session's instinct on seeing a dirty tree is to tidy it, and
348
+ **Stopping writing does not transfer the workspace.** Never tell the child "the tree is yours now".
349
+ That sentence describes your intent at one moment; it is not a property of the tree. In the first
350
+ real run the successor read it as permission and switched the shared checkout within the hour.
351
+
352
+ **The guard enforces this in Claude Code.** While a handshake in `.caddis/spawn-session/` is open,
353
+ the caddis guard denies `git switch`, `checkout`, `restore`, `stash`, `reset`, `rebase` and `clean`
354
+ in either session, and names the worktree command instead. The lock lifts at `handshake close`. A
355
+ handshake older than 24 hours is treated as abandoned and does not hold it. The agy guard does not
356
+ carry this lock yet, so on agy the prompt below is the only protection.
357
+
358
+ If the child needs another branch before close, it adds a worktree **at that moment**, not at spawn,
359
+ and removes it when the work lands. Leftover worktrees already pile up on this machine, and one made
360
+ eagerly adds to the pile. One consumer repo's `AGENTS.md` records that `git worktree remove --force`
361
+ can follow a junction and delete its target, so check a worktree for junctions before removing it.
362
+
363
+ **Still say it in the prompt.** A fresh session's instinct on seeing a dirty tree is to tidy it, and
315
364
  you are still in that tree.
316
365
 
317
366
  > While both sessions are live, do **not** run `git checkout`, `switch`, `stash`, `reset`,
@@ -331,7 +380,7 @@ See `.caddis/kb/shared-worktree-branch-switch.md` for detection and the recovery
331
380
  Three lines — messaged directly, or carried by the user on the paste route:
332
381
 
333
382
  ```
334
- SPAWN <id> | head <sha> | <n> tests | answers in .caddis/spawn-session/<id>-answers.md
383
+ SPAWN <id> | head <sha> | <n> tests | <n> tasks revived | answers in .caddis/spawn-session/<id>-answers.md
335
384
  Q1-Q6 answered with citations. <n> commands run. <n> facts I could not find in a file.
336
385
  <one line: the single most important thing I believe about the current state>
337
386
  ```
@@ -378,7 +427,7 @@ means regenerate" was unenforceable, because nothing counted them.
378
427
  ### When the fault is yours, fix the class
379
428
 
380
429
  If validation finds an error that came from your handover, **fix the source document in the same
381
- turn** — and fix the class, not the instance. In the manual run a successor found a stale commit
430
+ turn, and commit and push it before you reply** — and fix the class, not the instance. In the manual run a successor found a stale commit
382
431
  hash; the fix was not a fresher hash, it was to **remove hashes from both documents** and write the
383
432
  command instead. A handover patched per-instance goes stale again on the next commit.
384
433
 
@@ -402,8 +451,8 @@ Round 1's verdict sets round 2's size:
402
451
  | ACCEPT WITH CORRECTIONS | a **small, reversible** item that writes nothing permanent; review closely |
403
452
  | REJECT | no task — re-read and re-answer |
404
453
 
405
- Review the result **by re-deriving it**, exactly as you reviewed the answers — not by reading the
406
- successor's account of it.
454
+ Review the result **by re-deriving it**, exactly as you reviewed the answers — through a fresh
455
+ subagent, not by reading the successor's account of it.
407
456
 
408
457
  ### Then stop gating
409
458
 
@@ -414,6 +463,27 @@ to remove.
414
463
 
415
464
  ---
416
465
 
466
+ ## The parent's ending
467
+
468
+ The first real run had no ending. The parent stayed live for hours after the handover: it graded
469
+ twice, corrected four documents, merged two PRs and relayed decisions — still paying the token cost
470
+ the handover exists to escape. The owner expected it to close once the successor was proven. It now
471
+ does, in this order:
472
+
473
+ 1. **Before you issue the prompt, land or name your in-flight work.** `preflight` lists local
474
+ branches that were never pushed or are ahead of their upstream. Push each one and name it in the
475
+ relay, with what the successor must wait for. Open PRs are not detected — list them yourself. In
476
+ the first run the successor nearly reverted a fix that sat on the parent's unmerged PR.
477
+ 2. **After round 2's review is sent**, run `handshake close`. The handshake reaches `acknowledged`
478
+ in round 1, but do not close before round 2's review is out. `close` stamps `closed_at`, which
479
+ lifts the guard's tree lock.
480
+ 3. **Commit and push the handshake file.**
481
+ 4. **Send the child one last message:** what you handed over, and that you are stopping.
482
+ 5. **EXIT.** Tell the product owner the terminal can be closed. Do not stay on as an adviser. The
483
+ successor has the repo, and the repo is the record.
484
+
485
+ ---
486
+
417
487
  ## Files — all three committed
418
488
 
419
489
  ```
@@ -439,6 +509,9 @@ chat cannot be reviewed**, and cannot be re-read when the next handover asks wha
439
509
  | Write a question whose answer is not in a committed file | it would test memory |
440
510
  | Store an answer key | it freezes your belief, and the child can read it. **`check` enforces this** — see round 1 |
441
511
  | Grade your own answers | you validate; the child answers |
512
+ | Grade from a nearly-full context | send each round to a fresh subagent; grading needs the repo, not your memory |
513
+ | Tell the child the tree is its own | stopping writing does not transfer the workspace; the guard holds the lock until `close` |
514
+ | Stay on after `close` | the parent's job ends there — see "The parent's ending" |
442
515
  | Close on an unanswered handshake | it is the same as never running one. `handshake close` refuses |
443
516
  | Read silence as success | a child that skipped the gate looks exactly like one still reading |
444
517
  | Proceed when the open-item count is unknown | that count is the integrity check |
@@ -50,6 +50,29 @@ RELAY_FRAME_FOOTER = "=== end session-context ==="
50
50
 
51
51
  _TRUTHY = {"1", "true", "yes", "on"}
52
52
 
53
+ # The freshness report (fetch, then say how far this checkout is from the remote default
54
+ # branch) is the ONE piece here that is imported rather than duplicated: the agy bundle ships
55
+ # hooks/repo_freshness.py beside this file, listed in runtime-targets.json. In the source repo
56
+ # it sits one directory over, so both layouts resolve. Fail open — an older bundle without it
57
+ # simply reports no freshness.
58
+ try:
59
+ _HOOKS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "hooks")
60
+ if os.path.isdir(_HOOKS_DIR) and _HOOKS_DIR not in sys.path:
61
+ sys.path.append(_HOOKS_DIR)
62
+ import repo_freshness # noqa: E402
63
+ except Exception:
64
+ repo_freshness = None # type: ignore[assignment]
65
+
66
+
67
+ def freshness_lines(root: str) -> list[str]:
68
+ """Where `root` stands against the remote default branch; [] on any problem."""
69
+ if repo_freshness is None:
70
+ return []
71
+ try:
72
+ return repo_freshness.report(root)
73
+ except Exception:
74
+ return []
75
+
53
76
 
54
77
  def is_headless() -> bool:
55
78
  """True when no human is watching — a run given its task explicitly needs no resume pointer."""
@@ -268,11 +291,22 @@ def main() -> None:
268
291
  if conversation:
269
292
  _mark_injected(root, conversation)
270
293
 
271
- if not blocks:
294
+ # agy has no SessionStart event, so this first invocation is also where the freshness
295
+ # report belongs. It is emitted even when there is no relay to inject, and it stays
296
+ # silent in a repo that is clean, level and on the default branch.
297
+ fresh: list[str] = []
298
+ for root in roots:
299
+ fresh.extend(freshness_lines(root))
300
+
301
+ if not blocks and not fresh:
272
302
  return
273
- message = (RELAY_FRAME_HEADER + "\n" + "\n\n".join(blocks)
274
- + "\n\n" + RELAY_FRAME_FOOTER)
275
- payload = json.dumps({"injectSteps": [{"ephemeralMessage": message}]})
303
+ parts = []
304
+ if blocks:
305
+ parts.append(RELAY_FRAME_HEADER + "\n" + "\n\n".join(blocks)
306
+ + "\n\n" + RELAY_FRAME_FOOTER)
307
+ if fresh:
308
+ parts.append("\n".join(fresh))
309
+ payload = json.dumps({"injectSteps": [{"ephemeralMessage": "\n\n".join(parts)}]})
276
310
  sys.stdout.write(payload)
277
311
  except Exception:
278
312
  return # fail open: no injection is always better than a broken invocation
@@ -1,7 +1,7 @@
1
1
  {
2
- "poolVersion": "1.3.101",
2
+ "poolVersion": "1.3.105",
3
3
  "bundles": {
4
- "antigravity-plugin": "1.3.101",
4
+ "antigravity-plugin": "1.3.105",
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.12",
3
+ "version": "0.4.14",
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",