Skip to content

Commit df806ab

Browse files
author
codejunkie99
committed
feat: make data dashboard natural language
1 parent dd578d3 commit df806ab

9 files changed

Lines changed: 337 additions & 59 deletions

File tree

.agent/skills/_index.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@ Constraints: all tests passing, no unresolved TODOs in diff,
2828
requires human approval for production.
2929

3030
## data-layer
31-
Cross-harness activity monitoring and dashboard exports.
32-
Triggers: "data layer", "dashboard", "agent analytics", "resource usage",
33-
"cron monitoring", "daily report", "tokens", "terminal dashboard"
31+
Cross-harness activity monitoring and dashboard exports. Use it as the
32+
injected dashboard surface when users ask naturally.
33+
Triggers: "data layer", "dashboard", "show me the dashboard",
34+
"what did my agents do", "agent analytics", "agent status", "resource usage",
35+
"usage report", "cron monitoring", "daily report", "tokens",
36+
"terminal dashboard", "TUI"
3437
Constraints: local-only by default; no screenshot delivery without explicit user
3538
approval; do not commit private `.agent/data-layer/` exports.
3639

.agent/skills/_manifest.jsonl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
{"name":"git-proxy","version":"2026-01-01","triggers":["commit","push","branch","merge","rebase","pull request","PR"],"tools":["bash"],"preconditions":[".git exists"],"constraints":["never force push to main","never force push to protected branches","run tests before push"],"category":"operations"}
44
{"name":"debug-investigator","version":"2026-01-01","triggers":["debug","why is this failing","investigate","stack trace","bug"],"tools":["bash","memory_reflect"],"preconditions":[],"constraints":["reproduce before fixing","fix root cause, not symptoms"],"category":"engineering"}
55
{"name":"deploy-checklist","version":"2026-01-01","triggers":["deploy","ship","release","go live"],"tools":["bash"],"preconditions":[],"constraints":["all tests passing","no unresolved TODOs in diff","requires human approval for production"],"category":"operations"}
6-
{"name":"data-layer","version":"2026-04-25","triggers":["data layer","dashboard","agent analytics","resource usage","cron monitoring","daily report","tokens","terminal dashboard","TUI"],"tools":["bash","git"],"preconditions":[".agent exists"],"constraints":["local-only by default","no screenshot delivery without explicit user approval","do not commit private .agent/data-layer exports"],"category":"operations"}
6+
{"name":"data-layer","version":"2026-04-26","triggers":["data layer","dashboard","show me the dashboard","what did my agents do","agent analytics","agent status","resource usage","usage report","cron monitoring","daily report","tokens","terminal dashboard","TUI"],"tools":["bash","git"],"preconditions":[".agent exists"],"constraints":["local-only by default","no screenshot delivery without explicit user approval","do not commit private .agent/data-layer exports"],"category":"operations"}
77
{"name":"data-flywheel","version":"2026-04-25","triggers":["data flywheel","trace to train","training traces","context cards","eval cases","approved runs","vertical intelligence"],"tools":["bash","git"],"preconditions":[".agent exists"],"constraints":["local-only by default","human-approved runs only","redaction required before trainable","do not train models"],"category":"operations"}
88
{"name":"design-md","version":"2026-04-26","triggers":["DESIGN.md","design.md","Google Stitch","Stitch","design tokens","design system","visual design"],"tools":["bash","memory_reflect"],"preconditions":["DESIGN.md exists at project root"],"constraints":["prefer DESIGN.md tokens over invented values","do not modify DESIGN.md unless the user explicitly asks","preserve unknown sections when an edit IS authorised","validate when tooling is available"],"category":"design"}

.agent/skills/data-layer/SKILL.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: data-layer
3-
version: 2026-04-25
4-
triggers: ["data layer", "dashboard", "agent analytics", "resource usage", "cron monitoring", "daily report", "tokens"]
3+
version: 2026-04-26
4+
triggers: ["data layer", "dashboard", "show me the dashboard", "what did my agents do", "agent analytics", "agent status", "resource usage", "usage report", "cron monitoring", "daily report", "tokens", "terminal dashboard", "TUI"]
55
tools: [bash, git]
66
preconditions: [".agent exists"]
77
constraints: ["local-only by default", "do not send screenshots without explicit user approval", "do not commit private .agent/data-layer exports"]
@@ -52,6 +52,22 @@ Default inputs:
5252
`AGENT_LEARNINGS.jsonl` is the shared activity log. Optional files let users add
5353
events from harnesses that do not automatically write rich events yet.
5454

55+
## Agent behavior
56+
57+
When this skill is injected, decide whether the user is asking to see local
58+
agent activity. Natural prompts such as "what did my agents do", "show me the
59+
dashboard", "how many tokens did we use", or "show last week by hour" should
60+
render the terminal dashboard directly. Do not make users remember flags.
61+
62+
Prefer passing the user's words to the exporter:
63+
64+
```bash
65+
python3 .agent/tools/data_layer_export.py show me last 7 days by hour
66+
```
67+
68+
If the user gives no range or bucket, run the default export. Explicit flags
69+
still work for scripts and should override the natural-language words.
70+
5571
## Export
5672

5773
Run:

.agent/tools/data_layer_export.py

Lines changed: 170 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,48 @@
1616
import html
1717
import json
1818
import os
19+
import re
20+
import sys
1921
from pathlib import Path
2022
from typing import Any
2123

2224
VALID_WINDOWS = {"7d", "30d", "90d", "all"}
2325
VALID_BUCKETS = {"hour", "day", "week", "month"}
2426

2527

28+
def _e(*codes: int) -> str:
29+
return f"\x1b[{';'.join(map(str, codes))}m"
30+
31+
32+
def _hex(value: str) -> str:
33+
value = value.lstrip("#")
34+
r, g, b = int(value[:2], 16), int(value[2:4], 16), int(value[4:6], 16)
35+
return f"\x1b[38;2;{r};{g};{b}m"
36+
37+
38+
RESET = _e(0)
39+
BOLD = _e(1)
40+
DIM = _e(2)
41+
PURPLE = _hex("#BF5AF2")
42+
BLUE = _hex("#0A84FF")
43+
GREEN = _hex("#30D158")
44+
ORANGE = _hex("#FF9F0A")
45+
MUTED = _hex("#636366")
46+
WHITE = _hex("#F5F5F7")
47+
48+
49+
def paint(text: Any, color: str, enabled: bool) -> str:
50+
return f"{color}{text}{RESET}" if enabled else str(text)
51+
52+
53+
def colored_stdout_enabled() -> bool:
54+
return not os.environ.get("NO_COLOR")
55+
56+
57+
def rail(color: bool = False) -> str:
58+
return paint("│", MUTED, color)
59+
60+
2661
def sha256(value: Any) -> str:
2762
return hashlib.sha256(str(value).encode("utf-8")).hexdigest()
2863

@@ -120,6 +155,82 @@ def cutoff_for(window: str) -> dt.datetime | None:
120155
return dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=days)
121156

122157

158+
def nearest_window(days: int) -> str:
159+
if days <= 7:
160+
return "7d"
161+
if days <= 30:
162+
return "30d"
163+
if days <= 90:
164+
return "90d"
165+
return "all"
166+
167+
168+
def parse_natural_language_request(text: str) -> dict[str, str]:
169+
request = " ".join(text.split())
170+
if not request:
171+
return {}
172+
173+
key = request.lower()
174+
parsed: dict[str, str] = {}
175+
176+
bucket_patterns = [
177+
("hour", (r"\bby hour\b", r"\bper hour\b", r"\bhourly\b", r"\beach hour\b")),
178+
("day", (r"\bby day\b", r"\bper day\b", r"\bdaily\b", r"\beach day\b")),
179+
("week", (r"\bby week\b", r"\bper week\b", r"\bweekly\b", r"\beach week\b")),
180+
("month", (r"\bby month\b", r"\bper month\b", r"\bmonthly\b", r"\beach month\b")),
181+
]
182+
for bucket, patterns in bucket_patterns:
183+
if any(re.search(pattern, key) for pattern in patterns):
184+
parsed["bucket"] = bucket
185+
break
186+
187+
if re.search(r"\b(all time|everything|all history|entire history)\b", key):
188+
parsed["window"] = "all"
189+
elif re.search(r"\b(today|last 24 hours|past 24 hours)\b", key):
190+
parsed.setdefault("bucket", "hour")
191+
parsed["window"] = "7d"
192+
elif re.search(r"\b(this week|past week|last week|weekly view)\b", key):
193+
parsed["window"] = "7d"
194+
elif re.search(r"\b(this month|past month|last month|monthly view)\b", key):
195+
parsed["window"] = "30d"
196+
elif re.search(r"\b(this quarter|past quarter|last quarter|quarterly view)\b", key):
197+
parsed["window"] = "90d"
198+
199+
match = re.search(
200+
r"\b(?:last|past|previous|prior|over|for)?\s*(\d+)\s*(hours?|hrs?|h|days?|d|weeks?|w|months?|mos?|mo)\b",
201+
key,
202+
)
203+
if match:
204+
amount = int(match.group(1))
205+
unit = match.group(2)
206+
if unit.startswith(("hour", "hr", "h")):
207+
days = max(1, (amount + 23) // 24)
208+
parsed.setdefault("bucket", "hour")
209+
elif unit.startswith(("week", "w")):
210+
days = amount * 7
211+
elif unit.startswith(("month", "mo")):
212+
days = amount * 30
213+
else:
214+
days = amount
215+
parsed["window"] = nearest_window(days)
216+
217+
return parsed
218+
219+
220+
def flag_was_provided(argv: list[str], flag: str) -> bool:
221+
return any(token == flag or token.startswith(f"{flag}=") for token in argv)
222+
223+
224+
def apply_natural_language_request(args: argparse.Namespace, argv: list[str]) -> None:
225+
request_text = " ".join(args.request).strip()
226+
args.request_text = " ".join(request_text.split())
227+
parsed = parse_natural_language_request(args.request_text)
228+
if parsed.get("window") and not flag_was_provided(argv, "--window"):
229+
args.window = parsed["window"]
230+
if parsed.get("bucket") and not flag_was_provided(argv, "--bucket"):
231+
args.bucket = parsed["bucket"]
232+
233+
123234
def inside_window(record: dict[str, Any], cutoff: dt.datetime | None) -> bool:
124235
if cutoff is None:
125236
return True
@@ -716,6 +827,7 @@ def build_summary(args: argparse.Namespace, quality: dict[str, Any], agent_event
716827
"project": args.project,
717828
"window": args.window,
718829
"bucket": args.bucket,
830+
"request": getattr(args, "request_text", "") or None,
719831
"privacy_model": "local_only",
720832
"counts": {
721833
"agent_events": len(agent_events),
@@ -836,9 +948,9 @@ def plain_bar(value: Any, max_value: float, width: int = 18) -> str:
836948
return "#" * filled + "-" * (width - filled)
837949

838950

839-
def top_table(rows: list[dict[str, Any]], label_field: str, value_field: str, secondary_value_field: str = "", limit: int = 5) -> list[str]:
951+
def top_table(rows: list[dict[str, Any]], label_field: str, value_field: str, secondary_value_field: str = "", limit: int = 5, color: bool = False) -> list[str]:
840952
if not rows:
841-
return [" no data yet"]
953+
return [f"{rail(color)} {paint('no data yet', MUTED, color)}"]
842954
top = rows[:limit]
843955
values = [
844956
(safe_num(row.get(value_field)) or 0) + (safe_num(row.get(secondary_value_field)) or 0)
@@ -848,11 +960,26 @@ def top_table(rows: list[dict[str, Any]], label_field: str, value_field: str, se
848960
lines = []
849961
for row, value in zip(top, values):
850962
label = str(row.get(label_field) or "unknown")[:22].ljust(22)
851-
lines.append(f" {label} [{plain_bar(value, max_value)}] {compact_value(value)}")
963+
bar = plain_bar(value, max_value)
964+
lines.append(
965+
f"{rail(color)} {paint(label, WHITE, color)} "
966+
f"[{paint(bar, BLUE, color)}] {paint(compact_value(value), f'{BOLD}{WHITE}', color)}"
967+
)
852968
return lines
853969

854970

855-
def render_terminal_dashboard(out_dir: Path) -> str:
971+
def metric_line(label: str, value: str, color: bool = False) -> str:
972+
return (
973+
f"{paint('◆', PURPLE, color)} {paint(label.ljust(14), DIM, color)} "
974+
f"{paint('...', MUTED, color)} {paint(value, f'{BOLD}{WHITE}', color)}"
975+
)
976+
977+
978+
def section_line(title: str, color: bool = False) -> str:
979+
return f"{rail(color)} {paint(title, f'{BOLD}{ORANGE}', color)}"
980+
981+
982+
def render_terminal_dashboard(out_dir: Path, color: bool = False) -> str:
856983
summary = json.loads((out_dir / "dashboard-summary.json").read_text(encoding="utf-8"))
857984
activity = json.loads((out_dir / "activity-series.json").read_text(encoding="utf-8"))
858985
categories = json.loads((out_dir / "category-summary.json").read_text(encoding="utf-8"))
@@ -863,33 +990,42 @@ def render_terminal_dashboard(out_dir: Path) -> str:
863990
counts = summary["counts"]
864991
latest_activity = activity[-1] if activity else {}
865992
lines = [
866-
"agentic-stack Data Layer - Terminal Dashboard",
867-
f"project={summary['project']} window={summary['window']} bucket={summary['bucket']} generated={summary['generated_at']}",
868-
"",
869-
"Resource Overview",
870-
f" Agent events : {compact_value(counts['agent_events'])}",
871-
f" Cron runs : {compact_value(counts['cron_runs'])}",
872-
f" Harnesses : {compact_value(counts['harnesses'])}",
873-
f" Active agents: {compact_value(counts['active_agents'])}",
874-
f" Tokens est. : {compact_value(resources['tokens_total_estimate'])}",
875-
f" Cost est. : {compact_value(resources['cost_estimate_usd'], prefix='$')}",
876-
"",
877-
"Latest Bucket",
878-
f" {latest_activity.get('bucket_start', 'no activity')} events={compact_value(latest_activity.get('agent_events'))} cron={compact_value(latest_activity.get('cron_runs'))} tokens={compact_value(latest_activity.get('tokens_total_estimate'))}",
879-
"",
880-
"Top Harnesses",
881-
*top_table(harnesses, "harness", "agent_events", "cron_runs"),
882-
"",
883-
"Top Workflows",
884-
*top_table(workflows, "workflow", "agent_events", "cron_runs"),
885-
"",
886-
"Top Categories",
887-
*top_table(categories, "category", "agent_events", "cron_runs"),
888-
"",
889-
f"Open in browser: {out_dir / 'dashboard.html'}",
890-
f"Terminal copy : {out_dir / 'dashboard.tui.txt'}",
891-
"Privacy : local-only; screenshots require explicit user approval",
993+
f"{paint('◇', PURPLE, color)} {paint('agentic-stack Data Layer', f'{BOLD}{WHITE}', color)} {paint('Terminal Dashboard', MUTED, color)}",
994+
rail(color),
995+
f"{rail(color)} project={summary['project']} window={summary['window']} bucket={summary['bucket']}",
996+
f"{rail(color)} generated={summary['generated_at']}",
892997
]
998+
if summary.get("request"):
999+
lines.append(f"{rail(color)} Request ... {paint(summary['request'], f'{BOLD}{WHITE}', color)}")
1000+
lines.extend([
1001+
rail(color),
1002+
section_line("Resource Overview", color),
1003+
metric_line("Agent events", compact_value(counts["agent_events"]), color),
1004+
metric_line("Cron runs", compact_value(counts["cron_runs"]), color),
1005+
metric_line("Harnesses", compact_value(counts["harnesses"]), color),
1006+
metric_line("Active agents", compact_value(counts["active_agents"]), color),
1007+
metric_line("Tokens est.", compact_value(resources["tokens_total_estimate"]), color),
1008+
metric_line("Cost est.", compact_value(resources["cost_estimate_usd"], prefix="$"), color),
1009+
rail(color),
1010+
section_line("Latest Bucket", color),
1011+
f"{rail(color)} {latest_activity.get('bucket_start', 'no activity')} "
1012+
f"events={compact_value(latest_activity.get('agent_events'))} "
1013+
f"cron={compact_value(latest_activity.get('cron_runs'))} "
1014+
f"tokens={compact_value(latest_activity.get('tokens_total_estimate'))}",
1015+
rail(color),
1016+
section_line("Top Harnesses", color),
1017+
*top_table(harnesses, "harness", "agent_events", "cron_runs", color=color),
1018+
rail(color),
1019+
section_line("Top Workflows", color),
1020+
*top_table(workflows, "workflow", "agent_events", "cron_runs", color=color),
1021+
rail(color),
1022+
section_line("Top Categories", color),
1023+
*top_table(categories, "category", "agent_events", "cron_runs", color=color),
1024+
rail(color),
1025+
f"{paint('└', MUTED, color)} Open in browser: {out_dir / 'dashboard.html'}",
1026+
f"{rail(color)} Terminal copy : {out_dir / 'dashboard.tui.txt'}",
1027+
f"{rail(color)} Privacy : local-only; screenshots require explicit user approval",
1028+
])
8931029
return "\n".join(lines) + "\n"
8941030

8951031

@@ -973,7 +1109,7 @@ def export(args: argparse.Namespace) -> Path:
9731109
write_json(out_dir / "dashboard-report.json", dashboard_report)
9741110
write_dashboard(out_dir / "dashboard.html", summary, activity, categories, harnesses, workflows, cron_runs, cron_timeline, kpis)
9751111
write_daily_report(out_dir / "daily-report.md", summary)
976-
(out_dir / "dashboard.tui.txt").write_text(render_terminal_dashboard(out_dir), encoding="utf-8")
1112+
(out_dir / "dashboard.tui.txt").write_text(render_terminal_dashboard(out_dir, color=False), encoding="utf-8")
9771113
return out_dir
9781114

9791115

@@ -990,12 +1126,14 @@ def main() -> int:
9901126
parser.add_argument("--timezone", default=os.environ.get("TZ", "UTC"))
9911127
parser.add_argument("--window", choices=sorted(VALID_WINDOWS), default="30d")
9921128
parser.add_argument("--bucket", choices=sorted(VALID_BUCKETS), default="day")
1129+
parser.add_argument("request", nargs="*", help="Optional natural language request, for example: show me last 7 days by hour")
9931130
args = parser.parse_args()
1131+
apply_natural_language_request(args, sys.argv[1:])
9941132
out_dir = export(args)
9951133
print(f"agentic-stack data layer export: {out_dir}")
9961134
print(f"dashboard_html={out_dir / 'dashboard.html'}")
9971135
print()
998-
print(render_terminal_dashboard(out_dir), end="")
1136+
print(render_terminal_dashboard(out_dir, color=colored_stdout_enabled()), end="")
9991137
return 0
10001138

10011139

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,34 @@ All notable changes to this project.
55
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.11.2] — 2026-04-26
9+
10+
Patch release. Makes the data-layer dashboard easier to access from coding
11+
tools by turning the injected `data-layer` skill into the natural-language
12+
dashboard surface.
13+
14+
### Added
15+
- **Injected dashboard behavior.** The `data-layer` skill now triggers on plain
16+
phrases such as "show me the dashboard", "what did my agents do",
17+
"agent status", and "usage report". When the model decides the user wants
18+
local agent activity, the skill tells it to render the terminal dashboard
19+
directly instead of making the user remember flags.
20+
- **Natural-language exporter requests.** The existing exporter now accepts
21+
requests such as
22+
`python3 .agent/tools/data_layer_export.py show me last 7 days by hour`.
23+
It maps common phrases to `--window` and `--bucket`; explicit flags still
24+
override the natural-language text for scripts and automation.
25+
26+
### Changed
27+
- The terminal dashboard now uses the same rail/marker visual language as the
28+
onboarding flow while keeping `dashboard.tui.txt` plain text with no ANSI
29+
escape codes.
30+
- `dashboard-summary.json` records the natural-language request that produced
31+
the export when one was provided.
32+
33+
### Migration
34+
No migration required. Existing flag-based commands still work.
35+
836
## [0.11.1] — 2026-04-26
937

1038
Patch release. Makes the data-layer dashboard visible directly in coding-tool

0 commit comments

Comments
 (0)