@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.
- package/bundles/antigravity-plugin/guard_agy.py +62 -13
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/repo_freshness.py +146 -0
- package/bundles/antigravity-plugin/scripts/caddis-run.ps1 +11 -0
- package/bundles/antigravity-plugin/scripts/caddis-run.sh +9 -0
- package/bundles/antigravity-plugin/scripts/caddis_gate.py +24 -3
- package/bundles/antigravity-plugin/scripts/caddis_governor.py +542 -0
- package/bundles/antigravity-plugin/scripts/caddis_host_guard.py +689 -0
- package/bundles/antigravity-plugin/scripts/caddis_inventory.py +7 -3
- package/bundles/antigravity-plugin/scripts/caddis_lanes.py +1632 -0
- package/bundles/antigravity-plugin/scripts/caddis_spawn.py +159 -14
- package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +59 -7
- package/bundles/antigravity-plugin/scripts/install-host-guard.ps1 +393 -0
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +30 -4
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +48 -1
- package/bundles/antigravity-plugin/skills/kb/SKILL.md +4 -0
- package/bundles/antigravity-plugin/skills/spawn-session/SKILL.md +89 -16
- package/bundles/antigravity-plugin/warm_start_agy.py +38 -4
- package/bundles/manifest.json +2 -2
- package/package.json +1 -1
|
@@ -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.
|
|
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**
|
|
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
|
|
174
|
-
exist. It only **notes**
|
|
175
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
###
|
|
346
|
+
### The tree is never handed over while two sessions are attached
|
|
313
347
|
|
|
314
|
-
**
|
|
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 —
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
package/bundles/manifest.json
CHANGED
package/package.json
CHANGED