@caddis/cli 0.4.64 → 0.4.66

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 (23) hide show
  1. package/bundles/antigravity-plugin/plugin.json +1 -1
  2. package/bundles/antigravity-plugin/scripts/caddis_fleet.py +104 -3
  3. package/bundles/antigravity-plugin/scripts/caddis_roadmap_page.py +205 -70
  4. package/bundles/antigravity-plugin/scripts/oss_review.py +77 -45
  5. package/bundles/antigravity-plugin/scripts/roadmap_assets/LICENSE-Lucide.txt +43 -0
  6. package/bundles/antigravity-plugin/scripts/roadmap_assets/{OFL-JetBrainsMono.txt → OFL-Atkinson.txt} +2 -2
  7. package/bundles/antigravity-plugin/scripts/roadmap_assets/fonts.css +3 -3
  8. package/bundles/antigravity-plugin/scripts/roadmap_assets/icons.svg +2 -0
  9. package/bundles/antigravity-plugin/skills/_registry.md +1 -1
  10. package/bundles/antigravity-plugin/skills/catchup/SKILL.md +11 -5
  11. package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +10 -7
  12. package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +1 -1
  13. package/bundles/antigravity-plugin/skills/fleet-close/SKILL.md +5 -2
  14. package/bundles/antigravity-plugin/skills/handoff/SKILL.md +59 -24
  15. package/bundles/antigravity-plugin/skills/roadmap/assets/plan-resume-prompt.md +3 -3
  16. package/bundles/antigravity-plugin/skills/use-model/SKILL.md +3 -1
  17. package/bundles/antigravity-plugin-extras/plugin.json +1 -1
  18. package/bundles/antigravity-plugin-extras/skills/_registry.md +1 -1
  19. package/bundles/antigravity-plugin-extras/skills/agent-orchestration/SKILL.md +2 -2
  20. package/bundles/antigravity-plugin-extras/skills/cross-review/SKILL.md +9 -8
  21. package/bundles/codex/.codex/skills/workflow/roadmap/assets/plan-resume-prompt.md +3 -3
  22. package/bundles/manifest.json +3 -3
  23. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "caddis",
3
- "version": "1.3.150",
3
+ "version": "1.3.152",
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
  }
@@ -295,8 +295,8 @@ def check_sheet(sheet_text: str, roster_text: str) -> list[str]:
295
295
 
296
296
  # ---- session presence -------------------------------------------------------------------------
297
297
 
298
- def whoami(session_id: str, name: str, root: Path) -> Path:
299
- """Write `<artifacts>/session-state/<sid>.name`: one tab-separated line `name repo root branch`."""
298
+ def _name_file(session_id: str, root: Path) -> Path:
299
+ """`<artifacts>/session-state/<sid>.name`, the file `whoami` writes."""
300
300
  slug = "".join(c for c in session_id if c.isalnum() or c in "-_")[:64]
301
301
  if not slug:
302
302
  raise ValueError("empty session id")
@@ -305,6 +305,21 @@ def whoami(session_id: str, name: str, root: Path) -> Path:
305
305
  art = Path(artifact_root(str(root)))
306
306
  except Exception:
307
307
  art = root / ".caddis"
308
+ return art / "session-state" / f"{slug}.name"
309
+
310
+
311
+ def session_name(session_id: str, root: Path) -> tuple[str, bool]:
312
+ """The session's display name and whether `whoami` recorded it; else the first 8 characters of the id."""
313
+ try:
314
+ line = _name_file(session_id, root).read_text(encoding="utf-8").split("\n", 1)[0].split("\t")[0].strip()
315
+ except (OSError, UnicodeDecodeError, ValueError):
316
+ line = ""
317
+ return (line, True) if line else (session_id[:8], False)
318
+
319
+
320
+ def whoami(session_id: str, name: str, root: Path) -> Path:
321
+ """Write `<artifacts>/session-state/<sid>.name`: one tab-separated line `name repo root branch`."""
322
+ out = _name_file(session_id, root)
308
323
  branch = "-"
309
324
  for cmd in (["symbolic-ref", "--short", "-q", "HEAD"], ["rev-parse", "--short", "HEAD"]): # branch, else detached
310
325
  proc = subprocess.run(["git", "-C", str(root), *cmd], capture_output=True, text=True,
@@ -312,12 +327,64 @@ def whoami(session_id: str, name: str, root: Path) -> Path:
312
327
  if proc.returncode == 0 and proc.stdout.strip():
313
328
  branch = proc.stdout.strip()
314
329
  break
315
- out = art / "session-state" / f"{slug}.name"
316
330
  out.parent.mkdir(parents=True, exist_ok=True)
317
331
  out.write_text(f"{name}\t{root}\t{branch}\n", encoding="utf-8")
318
332
  return out
319
333
 
320
334
 
335
+ # ---- resume block -----------------------------------------------------------------------------
336
+
337
+ class ResumeError(ValueError):
338
+ """The relay has no usable `## Resume prompt`."""
339
+
340
+
341
+ _RESUME_SECTION = re.compile(r"^## Resume prompt[^\n]*\n(.*?)(?=^## |\Z)", re.MULTILINE | re.DOTALL)
342
+ _FENCE_OPEN = re.compile(r"^(`{3,}|~{3,})[^\n`]*$")
343
+
344
+
345
+ def resume_prompt(relay_text: str) -> str:
346
+ """The text inside the first fenced block of the relay's `## Resume prompt` section, verbatim."""
347
+ m = _RESUME_SECTION.search(relay_text)
348
+ if not m:
349
+ raise ResumeError("relay has no ## Resume prompt section")
350
+ body: list[str] = []
351
+ fence: str | None = None
352
+ for line in m.group(1).splitlines():
353
+ if fence is None:
354
+ opened = _FENCE_OPEN.match(line.strip())
355
+ if opened:
356
+ fence = opened.group(1)
357
+ continue
358
+ if line.strip().startswith(fence) and not line.strip().strip(fence[0]):
359
+ break # the closing fence
360
+ body.append(line)
361
+ else:
362
+ if fence is None:
363
+ raise ResumeError("## Resume prompt section has no fenced block")
364
+ raise ResumeError("## Resume prompt fenced block is not closed")
365
+ prompt = "\n".join(body).strip("\n")
366
+ if not prompt.strip():
367
+ raise ResumeError("## Resume prompt fenced block is empty")
368
+ return prompt
369
+
370
+
371
+ def shell_arg(text: str) -> str:
372
+ """One word that PowerShell and bash read the same: one line, inside single quotes (no expansion in either).
373
+ A `'` would end the word in bash and a `"` is dropped by Windows PowerShell 5.1, so both become typographic."""
374
+ one_line = " ".join(text.split())
375
+ return "'" + one_line.replace("'", "’").replace('"', "”") + "'"
376
+
377
+
378
+ def render_resume(session: str, session_id: str, workstream: str, prompt: str, role: str, stamp: datetime,
379
+ fleet_mode: bool = False) -> str:
380
+ """The last output of `/caddis:handoff`. In a fleet close the restart sheet holds the only start line."""
381
+ if fleet_mode:
382
+ start = "Start line: from the restart sheet"
383
+ else:
384
+ start = f"Start line:\nclaude --name {new_name(role, stamp)} {shell_arg(prompt)}"
385
+ return (f"=== RESUME ===\nSession: {session} ({session_id})\nWorkstream: {workstream}\n"
386
+ f"{_fenced(prompt)}\n{start}\n")
387
+
321
388
  # ---- CLI --------------------------------------------------------------------------------------
322
389
 
323
390
  def _load_acks(fleet_dir: Path) -> list[Ack]:
@@ -327,6 +394,32 @@ def _load_acks(fleet_dir: Path) -> list[Ack]:
327
394
  return [parse_ack(p.read_text(encoding="utf-8")) for p in sorted(acks_dir.glob("*.md"))]
328
395
 
329
396
 
397
+ def _resume_block_cli(args: argparse.Namespace) -> int:
398
+ try:
399
+ import caddis_workstreams
400
+ except ModuleNotFoundError as exc:
401
+ if exc.name != "caddis_workstreams":
402
+ raise
403
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
404
+ import caddis_workstreams
405
+ root = args.root.resolve()
406
+ try:
407
+ text, digest = caddis_workstreams.read_relay(root, args.name)
408
+ if digest is None:
409
+ raise ResumeError(f"relay {args.name!r} not found")
410
+ prompt = resume_prompt(text)
411
+ except (ValueError, OSError) as exc: # ResumeError is a ValueError; a bad relay name is one too
412
+ sys.stderr.write(f"resume-block failed: {exc}\n")
413
+ return caddis_exit.BLOCKED
414
+ session, known = session_name(args.session_id, root)
415
+ role = args.role or (role_of(session) if known else args.name)
416
+ if not _SAFE_NAME.match(role): # the role goes unquoted into the start line
417
+ sys.stderr.write(f"resume-block failed: bad role {role!r}\n")
418
+ return caddis_exit.BLOCKED
419
+ print(render_resume(session, args.session_id, args.name, prompt, role, datetime.now(), args.fleet), end="")
420
+ return caddis_exit.CLEAN
421
+
422
+
330
423
  def main(argv: list[str] | None = None) -> int:
331
424
  parser = argparse.ArgumentParser(prog="caddis_fleet.py", description=__doc__.splitlines()[0])
332
425
  sub = parser.add_subparsers(dest="cmd", required=True)
@@ -353,6 +446,12 @@ def main(argv: list[str] | None = None) -> int:
353
446
  p.add_argument("--session-id", required=True)
354
447
  p.add_argument("--name", required=True)
355
448
  p.add_argument("--root", default=".", type=Path)
449
+ p = sub.add_parser("resume-block", help="print the session name and the relay's resume prompt (last step of handoff)")
450
+ p.add_argument("--name", required=True, help="the relay's workstream name")
451
+ p.add_argument("--session-id", required=True)
452
+ p.add_argument("--root", default=".", type=Path)
453
+ p.add_argument("--role", help="role for the new session's name (default: the session name's role, else the relay name)")
454
+ p.add_argument("--fleet", action="store_true", help="fleet close: print no start line (the restart sheet has it)")
356
455
  args = parser.parse_args(argv)
357
456
 
358
457
  if args.cmd == "role-of":
@@ -419,6 +518,8 @@ def main(argv: list[str] | None = None) -> int:
419
518
  return caddis_exit.BLOCKED
420
519
  print("[FLEET] sheet ok")
421
520
  return caddis_exit.CLEAN
521
+ if args.cmd == "resume-block":
522
+ return _resume_block_cli(args)
422
523
  # whoami
423
524
  try:
424
525
  out = whoami(args.session_id, args.name, args.root.resolve())