@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.
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/scripts/caddis_fleet.py +104 -3
- package/bundles/antigravity-plugin/scripts/caddis_roadmap_page.py +205 -70
- package/bundles/antigravity-plugin/scripts/oss_review.py +77 -45
- package/bundles/antigravity-plugin/scripts/roadmap_assets/LICENSE-Lucide.txt +43 -0
- package/bundles/antigravity-plugin/scripts/roadmap_assets/{OFL-JetBrainsMono.txt → OFL-Atkinson.txt} +2 -2
- package/bundles/antigravity-plugin/scripts/roadmap_assets/fonts.css +3 -3
- package/bundles/antigravity-plugin/scripts/roadmap_assets/icons.svg +2 -0
- package/bundles/antigravity-plugin/skills/_registry.md +1 -1
- package/bundles/antigravity-plugin/skills/catchup/SKILL.md +11 -5
- package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +10 -7
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/fleet-close/SKILL.md +5 -2
- package/bundles/antigravity-plugin/skills/handoff/SKILL.md +59 -24
- package/bundles/antigravity-plugin/skills/roadmap/assets/plan-resume-prompt.md +3 -3
- package/bundles/antigravity-plugin/skills/use-model/SKILL.md +3 -1
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/_registry.md +1 -1
- package/bundles/antigravity-plugin-extras/skills/agent-orchestration/SKILL.md +2 -2
- package/bundles/antigravity-plugin-extras/skills/cross-review/SKILL.md +9 -8
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/plan-resume-prompt.md +3 -3
- package/bundles/manifest.json +3 -3
- package/package.json +1 -1
|
@@ -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
|
|
299
|
-
"""
|
|
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())
|