@seanyao/roll 2026.522.1 → 2026.523.1
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/CHANGELOG.md +36 -0
- package/bin/dream-test-quality-scan +110 -0
- package/bin/roll +527 -84
- package/conventions/config.yaml +1 -1
- package/conventions/global/AGENTS.md +1 -1
- package/conventions/global/GEMINI.md +8 -3
- package/conventions/templates/backend-service/GEMINI.md +3 -3
- package/conventions/templates/cli/GEMINI.md +3 -3
- package/conventions/templates/frontend-only/GEMINI.md +3 -3
- package/conventions/templates/fullstack/GEMINI.md +3 -3
- package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_render.cpython-314.pyc +0 -0
- package/lib/loop-fmt.py +17 -3
- package/lib/model_prices.py +16 -5
- package/lib/roll-loop-status.py +92 -21
- package/lib/roll-peer.py +1 -1
- package/lib/roll-status.py +1 -1
- package/lib/roll_render.py +16 -3
- package/lib/slides/templates/introduction-v3.html +576 -0
- package/package.json +1 -1
- package/skills/roll-.dream/SKILL.md +59 -0
- package/skills/roll-deck/SKILL.md +22 -14
- package/skills/roll-design/SKILL.md +90 -3
- package/skills/roll-doctor/SKILL.md +1 -1
- package/skills/roll-notes/SKILL.md +6 -3
- package/skills/roll-onboard/SKILL.md +1 -1
package/conventions/config.yaml
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
sync_claude: ~/.claude/CLAUDE.md
|
|
8
8
|
sync_kimi: ~/.kimi/AGENTS.md
|
|
9
9
|
sync_codex: ~/.codex/AGENTS.md
|
|
10
|
-
|
|
10
|
+
sync_agy: ~/.gemini/GEMINI.md # Antigravity (agy) reuses the legacy ~/.gemini/ path
|
|
11
11
|
|
|
12
12
|
# User preferences
|
|
13
13
|
default_language: zh
|
|
@@ -133,7 +133,7 @@ conventions, deploy targets.
|
|
|
133
133
|
|---|---|---|
|
|
134
134
|
| `conventions/global/AGENTS.md` | `conventions/templates/<type>/AGENTS.md` | All agents |
|
|
135
135
|
| `conventions/global/CLAUDE.md` | `conventions/templates/<type>/CLAUDE.md` | Claude Code |
|
|
136
|
-
| `conventions/global/GEMINI.md` | `conventions/templates/<type>/GEMINI.md` |
|
|
136
|
+
| `conventions/global/GEMINI.md` | `conventions/templates/<type>/GEMINI.md` | Antigravity (agy) — reads `~/.gemini/GEMINI.md` natively |
|
|
137
137
|
| `conventions/global/project_rules.md` | `conventions/templates/<type>/project_rules.md` | Trae IDE |
|
|
138
138
|
|
|
139
139
|
The CLAUDE / GEMINI / project_rules global files themselves declare they
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
# Global Preferences —
|
|
1
|
+
# Global Preferences — Antigravity (agy)
|
|
2
2
|
|
|
3
3
|
> Extends AGENTS.md in this directory — read that first for shared conventions.
|
|
4
|
-
> This file adds
|
|
4
|
+
> This file adds Antigravity-specific configuration only.
|
|
5
|
+
>
|
|
6
|
+
> File is named `GEMINI.md` because agy reads `~/.gemini/GEMINI.md` natively
|
|
7
|
+
> (it reuses the legacy Gemini CLI config dir). The filename is the canonical
|
|
8
|
+
> agy-side filename; the content here is for agy.
|
|
5
9
|
|
|
6
|
-
##
|
|
10
|
+
## Antigravity-Specific
|
|
7
11
|
|
|
8
12
|
- When running shell commands, prefer the most specific tool available.
|
|
9
13
|
- When a project has Roll workflow, follow the AGENTS.md conventions and use Roll skills.
|
|
14
|
+
- Invoke interactively with `agy -i "<prompt>"`; non-interactive with `agy -p "<prompt>"`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Project Preferences — Backend Service (
|
|
1
|
+
# Project Preferences — Backend Service (Antigravity)
|
|
2
2
|
|
|
3
|
-
> Extends global GEMINI.md + project AGENTS.md.
|
|
3
|
+
> Extends global GEMINI.md (Antigravity) + project AGENTS.md.
|
|
4
4
|
|
|
5
5
|
## Stack
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- Database: Prisma or Drizzle ORM
|
|
9
9
|
- Testing: Vitest + Supertest
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Antigravity (agy) Notes
|
|
12
12
|
|
|
13
13
|
- No frontend in this project. API-only service.
|
|
14
14
|
- Write integration tests that hit real endpoints, not mocked handlers.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Project Preferences — CLI Tool (
|
|
1
|
+
# Project Preferences — CLI Tool (Antigravity)
|
|
2
2
|
|
|
3
|
-
> Extends global GEMINI.md + project AGENTS.md.
|
|
3
|
+
> Extends global GEMINI.md (Antigravity) + project AGENTS.md.
|
|
4
4
|
|
|
5
5
|
## Stack
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- CLI framework: commander or citty
|
|
9
9
|
- Testing: Vitest
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Antigravity (agy) Notes
|
|
12
12
|
|
|
13
13
|
- No server, no frontend. CLI tool only.
|
|
14
14
|
- Test commands by running them, not just unit tests.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# Project Preferences — Frontend Only (
|
|
1
|
+
# Project Preferences — Frontend Only (Antigravity)
|
|
2
2
|
|
|
3
|
-
> Extends global GEMINI.md + project AGENTS.md.
|
|
3
|
+
> Extends global GEMINI.md (Antigravity) + project AGENTS.md.
|
|
4
4
|
|
|
5
5
|
## Stack
|
|
6
6
|
|
|
7
7
|
- React + shadcn/ui + Tailwind CSS + Vite
|
|
8
8
|
- Testing: Vitest + Playwright
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Antigravity (agy) Notes
|
|
11
11
|
|
|
12
12
|
- No backend in this project. All data via external API consumption.
|
|
13
13
|
- Run `npm run build` to verify production bundle compiles before pushing.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Project Preferences — Fullstack Web (
|
|
1
|
+
# Project Preferences — Fullstack Web (Antigravity)
|
|
2
2
|
|
|
3
|
-
> Extends global GEMINI.md + project AGENTS.md.
|
|
3
|
+
> Extends global GEMINI.md (Antigravity) + project AGENTS.md.
|
|
4
4
|
|
|
5
5
|
## Stack
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- Backend: Node.js API (Express/Hono/Fastify)
|
|
9
9
|
- Testing: Vitest (unit) + Playwright (E2E)
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Antigravity (agy) Notes
|
|
12
12
|
|
|
13
13
|
- When modifying API contracts, update both `api/types.ts` and `src/shared/types/` in the same commit.
|
|
14
14
|
- Run `npm run build` to verify both frontend and backend compile before pushing.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/loop-fmt.py
CHANGED
|
@@ -353,14 +353,28 @@ class LoopFmt:
|
|
|
353
353
|
# Use the cumulative totals accumulated across all assistant turns;
|
|
354
354
|
# result.usage is per-turn (last only) so it would under-count badly.
|
|
355
355
|
model = result_ev.get("model") or self._last_model or ""
|
|
356
|
+
|
|
357
|
+
# FIX-099: skip writing the usage event when claude returned no real
|
|
358
|
+
# usage data (model empty AND cost/duration both zero). This prevents
|
|
359
|
+
# stale/placeholder values from leaking into the events stream and
|
|
360
|
+
# showing up as "cost=$1.24 dur=372s" in three consecutive cycles when
|
|
361
|
+
# the real cycle had no token data (the default-value fallback).
|
|
362
|
+
# The dashboard can render "n/a" for missing usage rather than false data.
|
|
363
|
+
has_model = bool(model)
|
|
364
|
+
has_tokens = any(self._usage_totals[k] > 0 for k in self._usage_totals)
|
|
365
|
+
has_cost = bool(cost_usd)
|
|
366
|
+
has_dur = bool(dur_ms)
|
|
367
|
+
if not has_model and not has_tokens and not has_cost and not has_dur:
|
|
368
|
+
return # nothing real to report — skip rather than persist zeros
|
|
369
|
+
|
|
356
370
|
payload = {
|
|
357
|
-
"model": model,
|
|
371
|
+
"model": model if has_model else None,
|
|
358
372
|
"input_tokens": self._usage_totals["input_tokens"],
|
|
359
373
|
"output_tokens": self._usage_totals["output_tokens"],
|
|
360
374
|
"cache_creation_tokens": self._usage_totals["cache_creation_tokens"],
|
|
361
375
|
"cache_read_tokens": self._usage_totals["cache_read_tokens"],
|
|
362
|
-
"cost_reported_usd": float(cost_usd
|
|
363
|
-
"duration_ms": int(dur_ms
|
|
376
|
+
"cost_reported_usd": float(cost_usd) if has_cost else None,
|
|
377
|
+
"duration_ms": int(dur_ms) if has_dur else None,
|
|
364
378
|
}
|
|
365
379
|
evfile = os.path.join(shared, "loop", f"events-{slug}.ndjson")
|
|
366
380
|
line = json.dumps({
|
package/lib/model_prices.py
CHANGED
|
@@ -12,15 +12,26 @@ to sonnet rates with a stderr warning so dashboards don't blank out.
|
|
|
12
12
|
import sys
|
|
13
13
|
from typing import Dict, Optional
|
|
14
14
|
|
|
15
|
-
# Rates per million tokens (USD).
|
|
15
|
+
# Rates per million tokens (USD). cache_create = 5-minute cache write (1.25x
|
|
16
|
+
# input). 1-hour cache writes (2x input) are not modeled — Roll loop uses the
|
|
17
|
+
# default 5m caching only.
|
|
18
|
+
# Source: https://platform.claude.com/docs/en/about-claude/pricing
|
|
16
19
|
PRICES: Dict[str, Dict[str, float]] = {
|
|
17
|
-
# Claude 4.x family
|
|
18
|
-
|
|
19
|
-
"claude-opus-4-
|
|
20
|
+
# Claude 4.x Opus family — 2026-05 repricing: Opus 4.5+ moved to
|
|
21
|
+
# $5/$25 base, 3x cheaper than Opus 4 / 4.1.
|
|
22
|
+
"claude-opus-4-7": {"in": 5.00, "out": 25.00, "cache_create": 6.25, "cache_read": 0.50},
|
|
23
|
+
"claude-opus-4-6": {"in": 5.00, "out": 25.00, "cache_create": 6.25, "cache_read": 0.50},
|
|
24
|
+
"claude-opus-4-5": {"in": 5.00, "out": 25.00, "cache_create": 6.25, "cache_read": 0.50},
|
|
25
|
+
"claude-opus-4-1": {"in": 15.00, "out": 75.00, "cache_create": 18.75, "cache_read": 1.50},
|
|
26
|
+
"claude-opus-4": {"in": 15.00, "out": 75.00, "cache_create": 18.75, "cache_read": 1.50},
|
|
27
|
+
# Claude 4.x Sonnet family.
|
|
20
28
|
"claude-sonnet-4-6": {"in": 3.00, "out": 15.00, "cache_create": 3.75, "cache_read": 0.30},
|
|
29
|
+
"claude-sonnet-4-5": {"in": 3.00, "out": 15.00, "cache_create": 3.75, "cache_read": 0.30},
|
|
21
30
|
"claude-sonnet-4": {"in": 3.00, "out": 15.00, "cache_create": 3.75, "cache_read": 0.30},
|
|
31
|
+
# Claude 4.x Haiku family.
|
|
22
32
|
"claude-haiku-4-5": {"in": 1.00, "out": 5.00, "cache_create": 1.25, "cache_read": 0.10},
|
|
23
|
-
# Older
|
|
33
|
+
# Older / retired models (Bedrock & Vertex only for 3.5 Haiku).
|
|
34
|
+
"claude-haiku-3-5": {"in": 0.80, "out": 4.00, "cache_create": 1.00, "cache_read": 0.08},
|
|
24
35
|
"claude-3-5-sonnet": {"in": 3.00, "out": 15.00, "cache_create": 3.75, "cache_read": 0.30},
|
|
25
36
|
}
|
|
26
37
|
|
package/lib/roll-loop-status.py
CHANGED
|
@@ -336,10 +336,16 @@ def load_claude_session_usage(label: str, slug: str) -> Optional[Dict[str, Any]]
|
|
|
336
336
|
"cost_reported_usd": cost, "duration_ms": duration_ms}
|
|
337
337
|
|
|
338
338
|
def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str) -> None:
|
|
339
|
-
"""Populate cy['
|
|
339
|
+
"""Populate cy['input_tokens'], cy['output_tokens'], cy['cost_list'],
|
|
340
|
+
cy['model']. Two paths:
|
|
340
341
|
1. usage_event from events stream (US-LOOP-004 writer side) — authoritative
|
|
341
342
|
2. claude session JSONL backfill — for cycles that ran before the
|
|
342
343
|
writer existed, or on machines where events.ndjson got truncated
|
|
344
|
+
|
|
345
|
+
US-VIEW-012: dashboard exposes input + output only (the model's actual
|
|
346
|
+
work). cache_creation / cache_read remain in the usage_event for
|
|
347
|
+
compute_list_cost — they're still part of true API cost — but no longer
|
|
348
|
+
surface in the UI where they previously inflated visible token totals.
|
|
343
349
|
"""
|
|
344
350
|
import importlib.util
|
|
345
351
|
spec = importlib.util.spec_from_file_location("model_prices",
|
|
@@ -350,12 +356,10 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
|
|
|
350
356
|
# Path 1: usage event written by loop-fmt at result time.
|
|
351
357
|
ue = cy.get("usage_event")
|
|
352
358
|
if isinstance(ue, dict) and (ue.get("input_tokens") or ue.get("output_tokens")):
|
|
353
|
-
cy["
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
cache_read_tokens=ue.get("cache_read_tokens", 0),
|
|
358
|
-
)
|
|
359
|
+
cy["input_tokens"] = int(ue.get("input_tokens") or 0)
|
|
360
|
+
cy["output_tokens"] = int(ue.get("output_tokens") or 0)
|
|
361
|
+
cy["cache_creation_tokens"] = int(ue.get("cache_creation_tokens") or 0)
|
|
362
|
+
cy["cache_read_tokens"] = int(ue.get("cache_read_tokens") or 0)
|
|
359
363
|
cy["model"] = ue.get("model")
|
|
360
364
|
# US-VIEW-010: aggregate now sums per-turn usage tokens, so the
|
|
361
365
|
# totals in `ue` reflect the whole cycle. Always compute cost at
|
|
@@ -373,17 +377,15 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
|
|
|
373
377
|
cy["duration_s"] = int(ue["duration_ms"] / 1000)
|
|
374
378
|
continue
|
|
375
379
|
# Path 2: salvage from claude's own session log.
|
|
376
|
-
if cy.get("
|
|
380
|
+
if cy.get("input_tokens") or cy.get("output_tokens"):
|
|
377
381
|
continue
|
|
378
382
|
u = load_claude_session_usage(cy.get("label", ""), slug)
|
|
379
383
|
if not u:
|
|
380
384
|
continue
|
|
381
|
-
cy["
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
cache_read_tokens=u["cache_read_tokens"],
|
|
386
|
-
)
|
|
385
|
+
cy["input_tokens"] = int(u.get("input_tokens") or 0)
|
|
386
|
+
cy["output_tokens"] = int(u.get("output_tokens") or 0)
|
|
387
|
+
cy["cache_creation_tokens"] = int(u.get("cache_creation_tokens") or 0)
|
|
388
|
+
cy["cache_read_tokens"] = int(u.get("cache_read_tokens") or 0)
|
|
387
389
|
cy["model"] = u["model"]
|
|
388
390
|
cy["cost_list"] = mp.compute_list_cost(
|
|
389
391
|
u["model"],
|
|
@@ -553,15 +555,27 @@ def bucket_by_day(cycles: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]
|
|
|
553
555
|
return out
|
|
554
556
|
|
|
555
557
|
def rollup_for_day(day_cycles: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
558
|
+
# US-VIEW-012: track input + output separately so the daily summary can
|
|
559
|
+
# show two metric rows. cache_read tokens deliberately excluded — they're
|
|
560
|
+
# already captured in cy["cost_list"] via list-price math (compute_list_cost
|
|
561
|
+
# reads all 4 fields), but they don't represent the model's actual work.
|
|
556
562
|
r = {"cycles": len(day_cycles), "prs": 0, "failed": 0,
|
|
557
|
-
"duration_s": 0, "cost": 0.0,
|
|
563
|
+
"duration_s": 0, "cost": 0.0,
|
|
564
|
+
"input_tokens": 0, "output_tokens": 0,
|
|
565
|
+
"cache_creation_tokens": 0, "cache_read_tokens": 0}
|
|
558
566
|
for cy in day_cycles:
|
|
559
567
|
if cy.get("outcome") == "fail":
|
|
560
568
|
r["failed"] += 1
|
|
561
569
|
if cy.get("duration_s"):
|
|
562
570
|
r["duration_s"] += cy["duration_s"]
|
|
563
|
-
if cy.get("
|
|
564
|
-
r["
|
|
571
|
+
if cy.get("input_tokens"):
|
|
572
|
+
r["input_tokens"] += cy["input_tokens"]
|
|
573
|
+
if cy.get("output_tokens"):
|
|
574
|
+
r["output_tokens"] += cy["output_tokens"]
|
|
575
|
+
if cy.get("cache_creation_tokens"):
|
|
576
|
+
r["cache_creation_tokens"] += cy["cache_creation_tokens"]
|
|
577
|
+
if cy.get("cache_read_tokens"):
|
|
578
|
+
r["cache_read_tokens"] += cy["cache_read_tokens"]
|
|
565
579
|
# US-VIEW-011: rollup only counts cycles whose PR actually merged.
|
|
566
580
|
# Backward compat: rows where pr_outcome is missing but pr URL exists
|
|
567
581
|
# (no `pr` event after the writer upgrade ran for that cycle) are
|
|
@@ -620,9 +634,26 @@ def render(events, cron, state, backlog, *, days=3, lang="both", now=None,
|
|
|
620
634
|
c("muted", " · ") + c("dim", state.get("paused_reason", "")))
|
|
621
635
|
eb_zh = c("dim", " 已暂停 · run: roll loop resume")
|
|
622
636
|
else:
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
637
|
+
# FIX-095: surface three-state install/enable status. Pre-FIX, every
|
|
638
|
+
# case fell through to '● IDLE' which hid 'not installed' and
|
|
639
|
+
# 'installed/off' from the user.
|
|
640
|
+
install_state = _detect_install_state()
|
|
641
|
+
if install_state == "not-installed":
|
|
642
|
+
eb_l = (c("muted", "○ not installed", bold=True) + c("muted", " ") +
|
|
643
|
+
c("dim", "run ") + c("fg", "roll loop on", bold=True) +
|
|
644
|
+
c("dim", " to enable"))
|
|
645
|
+
eb_zh = c("dim", " 未安装 · 运行 ") + c("fg", "roll loop on") + c("dim", " 启用")
|
|
646
|
+
elif install_state in ("stale", "disabled"):
|
|
647
|
+
# FIX-098: 'stale' = plist on disk but agent not registered in launchd.
|
|
648
|
+
# 'disabled' kept for back-compat (old install_state values). Both mean
|
|
649
|
+
# the user needs to run 'roll loop on' to bootstrap the agent.
|
|
650
|
+
eb_l = (c("amber", "◌ STALE — plist present, not loaded", bold=True) + c("muted", " ") +
|
|
651
|
+
c("dim", "run ") + c("fg", "roll loop on", bold=True) + c("dim", " to repair"))
|
|
652
|
+
eb_zh = c("dim", " Plist 存在但未加载 · 运行 ") + c("fg", "roll loop on") + c("dim", " 修复")
|
|
653
|
+
else:
|
|
654
|
+
eb_l = (c("blue", "● IDLE", bold=True) + c("muted", " · ") +
|
|
655
|
+
c("dim", "enabled · next run ") + c("fg", _next_cron_hint(state), bold=True))
|
|
656
|
+
eb_zh = c("dim", f" 已启用 · 闲置 · 距下一轮 {_next_cron_hint(state, zh=True)}")
|
|
626
657
|
|
|
627
658
|
# 'last' = the most recent cycle the user can act on — skip cycles that
|
|
628
659
|
# are still running (the running banner already announces those) and skip
|
|
@@ -704,7 +735,13 @@ def render(events, cron, state, backlog, *, days=3, lang="both", now=None,
|
|
|
704
735
|
yest_color="amber" if yest["failed"] > 0 else "dim",
|
|
705
736
|
yest_suffix="⚠" if yest["failed"] > 0 else "")
|
|
706
737
|
metric_dur("duration", today["duration_s"], yest["duration_s"], d2["duration_s"], partial=is_partial)
|
|
707
|
-
|
|
738
|
+
# US-VIEW-017: show all 4 token components so the cost is explainable.
|
|
739
|
+
# cache_creation (↑) and cache_read (↓) typically account for 80-90% of
|
|
740
|
+
# cost — hiding them makes the cost line incomprehensible.
|
|
741
|
+
metric_tokens("input tokens", today["input_tokens"], yest["input_tokens"], d2["input_tokens"], partial=is_partial)
|
|
742
|
+
metric_tokens("cache writes", today["cache_creation_tokens"], yest["cache_creation_tokens"], d2["cache_creation_tokens"], partial=is_partial)
|
|
743
|
+
metric_tokens("cache reads", today["cache_read_tokens"], yest["cache_read_tokens"], d2["cache_read_tokens"], partial=is_partial)
|
|
744
|
+
metric_tokens("output tokens", today["output_tokens"], yest["output_tokens"], d2["output_tokens"], partial=is_partial)
|
|
708
745
|
metric_dollar("cost", today["cost"], yest["cost"], d2["cost"], partial=is_partial)
|
|
709
746
|
|
|
710
747
|
print()
|
|
@@ -759,6 +796,40 @@ def _read_plist_loop_minute() -> int:
|
|
|
759
796
|
return int(m.group(1)) if m else 48
|
|
760
797
|
|
|
761
798
|
|
|
799
|
+
def _detect_install_state() -> str:
|
|
800
|
+
"""FIX-095 / FIX-098: classify the launchd install state of the loop service.
|
|
801
|
+
|
|
802
|
+
Returns one of:
|
|
803
|
+
'not-installed' — no plist for com.roll.loop.<slug> in ~/Library/LaunchAgents/
|
|
804
|
+
'stale' — plist on disk but agent NOT registered in launchd
|
|
805
|
+
(happens after roll loop off + roll update without roll loop on)
|
|
806
|
+
'enabled' — plist on disk AND registered in launchd
|
|
807
|
+
|
|
808
|
+
FIX-098: switched from `launchctl print-disabled` (disabled-overrides DB) to
|
|
809
|
+
`launchctl print gui/<uid>/<label>` which probes the actual launchd registry.
|
|
810
|
+
The old approach returned false-positive 'enabled' when the disabled-overrides
|
|
811
|
+
DB had no entry for the label (empty = not explicitly disabled, not loaded).
|
|
812
|
+
"""
|
|
813
|
+
slug = project_slug()
|
|
814
|
+
label = f"com.roll.loop.{slug}"
|
|
815
|
+
plist = Path(os.path.expanduser("~/Library/LaunchAgents")) / f"{label}.plist"
|
|
816
|
+
if not plist.exists():
|
|
817
|
+
return "not-installed"
|
|
818
|
+
try:
|
|
819
|
+
uid = os.getuid()
|
|
820
|
+
result = subprocess.run(
|
|
821
|
+
["launchctl", "print", f"gui/{uid}/{label}"],
|
|
822
|
+
capture_output=True, timeout=2,
|
|
823
|
+
)
|
|
824
|
+
if result.returncode == 0:
|
|
825
|
+
return "enabled"
|
|
826
|
+
return "stale"
|
|
827
|
+
except Exception:
|
|
828
|
+
# launchctl missing or timed out — assume stale (safe: user sees STALE
|
|
829
|
+
# banner and is told to run 'roll loop on' to repair).
|
|
830
|
+
return "stale"
|
|
831
|
+
|
|
832
|
+
|
|
762
833
|
def _next_cron_hint(state: Dict[str, str], zh: bool = False) -> str:
|
|
763
834
|
"""Compute next cron fire time from the actual launchd plist Minute (FIX-063)."""
|
|
764
835
|
now = datetime.now().astimezone()
|
package/lib/roll-peer.py
CHANGED
package/lib/roll-status.py
CHANGED
|
@@ -160,7 +160,7 @@ def _fixture_data() -> Dict[str, Any]:
|
|
|
160
160
|
ai_clients=[
|
|
161
161
|
{"name": "claude", "cfg_file": "CLAUDE.md", "path": "~/.claude/CLAUDE.md", "sync": "sync", "skills": 12},
|
|
162
162
|
{"name": "cursor", "cfg_file": "AGENTS.md", "path": "~/.cursor/AGENTS.md", "sync": "out-of-sync", "skills": 12},
|
|
163
|
-
{"name": "
|
|
163
|
+
{"name": "agy", "cfg_file": "GEMINI.md", "path": "~/.gemini/GEMINI.md", "sync": "missing", "skills": 0},
|
|
164
164
|
],
|
|
165
165
|
templates=[
|
|
166
166
|
("fullstack", 14), ("frontend-only", 9), ("backend-service", 11), ("cli", 7),
|
package/lib/roll_render.py
CHANGED
|
@@ -159,7 +159,8 @@ def trunc(s: str, n: int) -> str:
|
|
|
159
159
|
return out
|
|
160
160
|
|
|
161
161
|
def empty_rollup() -> Dict[str, Any]:
|
|
162
|
-
return {"cycles": 0, "prs": 0, "failed": 0, "duration_s": 0, "cost": 0.0,
|
|
162
|
+
return {"cycles": 0, "prs": 0, "failed": 0, "duration_s": 0, "cost": 0.0,
|
|
163
|
+
"input_tokens": 0, "output_tokens": 0}
|
|
163
164
|
|
|
164
165
|
# ════════════════════════════════════════════════════════════════════════════
|
|
165
166
|
# Section / metric / cycle rows — printers used by all dashboards
|
|
@@ -297,7 +298,19 @@ def cycle_row(cy: Dict[str, Any], backlog: Dict[str, str]) -> None:
|
|
|
297
298
|
from datetime import datetime as _dt, timezone as _tz
|
|
298
299
|
dur_s = int((_dt.now(_tz.utc) - cy["start"]).total_seconds())
|
|
299
300
|
dur = fmt_dur(dur_s) if dur_s else "—"
|
|
300
|
-
|
|
301
|
+
# US-VIEW-017: show all 4 token components when cache data is available.
|
|
302
|
+
# Format: "in/cw↑ cr↓/out" (cache writes ↑, cache reads ↓).
|
|
303
|
+
# Falls back to "in/out" for cycles that predate cache tracking.
|
|
304
|
+
inp = cy.get('input_tokens') or 0
|
|
305
|
+
out_tok = cy.get('output_tokens') or 0
|
|
306
|
+
cw = cy.get('cache_creation_tokens') or 0
|
|
307
|
+
cr = cy.get('cache_read_tokens') or 0
|
|
308
|
+
if cw or cr:
|
|
309
|
+
tok = (f"{fmt_tokens(inp)}"
|
|
310
|
+
f"/{fmt_tokens(cw)}↑ {fmt_tokens(cr)}↓"
|
|
311
|
+
f"/{fmt_tokens(out_tok)}")
|
|
312
|
+
else:
|
|
313
|
+
tok = f"{fmt_tokens(inp)}/{fmt_tokens(out_tok)}"
|
|
301
314
|
# cost prefers the backfilled list-price; falls back to cron.log when
|
|
302
315
|
# the claude session log isn't available (only the latest cycle).
|
|
303
316
|
if cy.get("cost_list") is not None:
|
|
@@ -341,7 +354,7 @@ def cycle_row(cy: Dict[str, Any], backlog: Dict[str, str]) -> None:
|
|
|
341
354
|
" " + c(glyph_c, glyph, bold=True) + " " +
|
|
342
355
|
c(time_c, pad(time_str, 5), bold=(outcome == "fail")) + " " +
|
|
343
356
|
c("muted", pad(dur, 4, "r")) + " " +
|
|
344
|
-
c("muted", pad(tok,
|
|
357
|
+
c("muted", pad(tok, 26)) + " " +
|
|
345
358
|
model_seg +
|
|
346
359
|
c("muted", pad(cost, 7, "r")) + " " +
|
|
347
360
|
c(sid_c, ids_str, bold=True) + pr_marker
|