agent-devkit 0.0.3 → 0.1.0

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 (57) hide show
  1. package/README.md +170 -1
  2. package/package.json +1 -1
  3. package/runtime/README.md +192 -14
  4. package/runtime/agent +29 -13
  5. package/runtime/agents/README.md +3 -0
  6. package/runtime/agents/github-pr-reviewer/AGENTS.md +10 -0
  7. package/runtime/agents/github-pr-reviewer/README.md +7 -0
  8. package/runtime/agents/github-pr-reviewer/agent.yaml +33 -0
  9. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/capability.yaml +20 -0
  10. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/decision-rules.md +8 -0
  11. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/workflow.md +9 -0
  12. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/capability.yaml +20 -0
  13. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/decision-rules.md +7 -0
  14. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/workflow.md +8 -0
  15. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/capability.yaml +20 -0
  16. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/decision-rules.md +7 -0
  17. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/workflow.md +8 -0
  18. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/capability.yaml +20 -0
  19. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/decision-rules.md +9 -0
  20. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/workflow.md +10 -0
  21. package/runtime/agents/github-pr-reviewer/infra/README.md +7 -0
  22. package/runtime/agents/github-pr-reviewer/knowledge/context.md +7 -0
  23. package/runtime/agents/github-pr-reviewer/knowledge/system.md +17 -0
  24. package/runtime/agents/github-pr-reviewer/templates/pr-automation-output.md +10 -0
  25. package/runtime/agents/github-pr-reviewer/templates/pr-inspection-output.md +9 -0
  26. package/runtime/agents/github-pr-reviewer/templates/pr-list-output.md +9 -0
  27. package/runtime/agents/github-pr-reviewer/templates/pr-review-output.md +17 -0
  28. package/runtime/cli/README.md +93 -0
  29. package/runtime/cli/aikit/__init__.py +1 -1
  30. package/runtime/cli/aikit/aliases.py +196 -0
  31. package/runtime/cli/aikit/app_home.py +78 -0
  32. package/runtime/cli/aikit/audit.py +344 -0
  33. package/runtime/cli/aikit/calendar.py +163 -0
  34. package/runtime/cli/aikit/diagnostics.py +9 -0
  35. package/runtime/cli/aikit/github_pr.py +254 -0
  36. package/runtime/cli/aikit/identity.py +75 -0
  37. package/runtime/cli/aikit/llm.py +240 -48
  38. package/runtime/cli/aikit/main.py +924 -24
  39. package/runtime/cli/aikit/memory.py +148 -8
  40. package/runtime/cli/aikit/notifications.py +9 -0
  41. package/runtime/cli/aikit/permissions.py +345 -0
  42. package/runtime/cli/aikit/personality.py +123 -0
  43. package/runtime/cli/aikit/providers.py +4 -5
  44. package/runtime/cli/aikit/scheduler.py +18 -0
  45. package/runtime/cli/aikit/sessions.py +396 -0
  46. package/runtime/cli/aikit/setup_wizard.py +12 -0
  47. package/runtime/cli/aikit/tasks.py +351 -0
  48. package/runtime/cli/aikit/toolchain.py +274 -0
  49. package/runtime/plugins/claude-code-ai-devkit/plugin.json +1 -1
  50. package/runtime/plugins/claude-skill-ai-devkit/plugin.json +1 -1
  51. package/runtime/providers/calendar.yaml +28 -0
  52. package/runtime/providers/github.yaml +42 -0
  53. package/runtime/providers/local-notification.yaml +19 -0
  54. package/runtime/providers/local-scheduler.yaml +24 -0
  55. package/runtime/scripts/README.md +1 -1
  56. package/runtime/scripts/verify-release-alignment.mjs +1 -1
  57. package/runtime/vendor/skills/napkin/napkin.md +13 -3
@@ -0,0 +1,344 @@
1
+ """Local audit trail for Agent DevKit CLI executions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import getpass
6
+ import json
7
+ import re
8
+ import uuid
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from cli.aikit.app_home import audit_home, ensure_app_home
14
+ from cli.aikit.memory import normalize_prompt, redact_secrets
15
+
16
+
17
+ AUDIT_VERSION = 1
18
+
19
+
20
+ def now_utc() -> datetime:
21
+ return datetime.now(timezone.utc)
22
+
23
+
24
+ def now_iso() -> str:
25
+ return now_utc().isoformat()
26
+
27
+
28
+ def ensure_audit_home() -> Path:
29
+ ensure_app_home()
30
+ home = audit_home()
31
+ home.mkdir(parents=True, exist_ok=True)
32
+ return home
33
+
34
+
35
+ def audit_day_home(day: str | None = None) -> Path:
36
+ resolved_day = day or now_utc().date().isoformat()
37
+ path = ensure_audit_home() / resolved_day
38
+ path.mkdir(parents=True, exist_ok=True)
39
+ return path
40
+
41
+
42
+ def record_audit(
43
+ *,
44
+ command: str | None,
45
+ args: dict[str, Any],
46
+ result: dict[str, Any] | None = None,
47
+ error: str | None = None,
48
+ ) -> dict[str, Any]:
49
+ created_at = now_iso()
50
+ execution_id = f"exec_{now_utc().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}"
51
+ prompt = extract_prompt(args)
52
+ safe_result = redact_value(result or {})
53
+ entry = {
54
+ "version": AUDIT_VERSION,
55
+ "id": execution_id,
56
+ "created_at": created_at,
57
+ "user": safe_user(),
58
+ "command": command,
59
+ "prompt": redact_secrets(prompt) if prompt else None,
60
+ "prompt_normalized": normalize_prompt(prompt) if prompt else None,
61
+ "session": extract_session(safe_result),
62
+ "route": safe_result.get("route"),
63
+ "agent": extract_agent(safe_result, args),
64
+ "providers": extract_providers(safe_result),
65
+ "sources": extract_sources(safe_result),
66
+ "commands": extract_commands(safe_result),
67
+ "permissions": extract_permissions(safe_result),
68
+ "llm_backends": extract_llm_backends(safe_result),
69
+ "token_estimate": extract_token_estimate(safe_result),
70
+ "external_actions": extract_external_actions(safe_result),
71
+ "result": summarize_result(safe_result),
72
+ "error": redact_secrets(error) if error else None,
73
+ "redaction_applied": True,
74
+ }
75
+ day_home = audit_day_home(created_at[:10])
76
+ json_path = day_home / f"{execution_id}.json"
77
+ md_path = day_home / f"{execution_id}.md"
78
+ json_path.write_text(json.dumps(entry, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
79
+ md_path.write_text(render_audit_md(entry), encoding="utf-8")
80
+ return {"id": execution_id, "json_path": str(json_path), "markdown_path": str(md_path)}
81
+
82
+
83
+ def list_audits(limit: int = 20) -> dict[str, Any]:
84
+ items: list[dict[str, Any]] = []
85
+ for path in sorted(ensure_audit_home().glob("*/*.json"), reverse=True):
86
+ try:
87
+ payload = json.loads(path.read_text(encoding="utf-8"))
88
+ except json.JSONDecodeError:
89
+ continue
90
+ if not isinstance(payload, dict):
91
+ continue
92
+ items.append(
93
+ {
94
+ "id": payload.get("id"),
95
+ "created_at": payload.get("created_at"),
96
+ "command": payload.get("command"),
97
+ "status": (payload.get("result") or {}).get("status"),
98
+ "ok": (payload.get("result") or {}).get("ok"),
99
+ "json_path": str(path),
100
+ "markdown_path": str(path.with_suffix(".md")),
101
+ }
102
+ )
103
+ if len(items) >= limit:
104
+ break
105
+ return {"kind": "audit", "status": "ok", "home": str(ensure_audit_home()), "items": items}
106
+
107
+
108
+ def show_audit(execution_id: str) -> dict[str, Any]:
109
+ path = find_audit_json(execution_id)
110
+ payload = json.loads(path.read_text(encoding="utf-8"))
111
+ return {
112
+ "kind": "audit-entry",
113
+ "status": "ok",
114
+ "id": payload.get("id"),
115
+ "entry": payload,
116
+ "json_path": str(path),
117
+ "markdown_path": str(path.with_suffix(".md")),
118
+ }
119
+
120
+
121
+ def export_audit(execution_id: str, *, fmt: str = "md") -> dict[str, Any]:
122
+ path = find_audit_json(execution_id)
123
+ if fmt == "json":
124
+ content = path.read_text(encoding="utf-8")
125
+ export_path = path
126
+ elif fmt == "md":
127
+ md_path = path.with_suffix(".md")
128
+ content = md_path.read_text(encoding="utf-8") if md_path.exists() else render_audit_md(json.loads(path.read_text(encoding="utf-8")))
129
+ export_path = md_path
130
+ else:
131
+ raise ValueError("--format must be md or json")
132
+ return {
133
+ "kind": "audit-export",
134
+ "status": "ok",
135
+ "id": execution_id,
136
+ "format": fmt,
137
+ "path": str(export_path),
138
+ "content": content,
139
+ }
140
+
141
+
142
+ def find_audit_json(execution_id: str) -> Path:
143
+ if not execution_id or "/" in execution_id or "\\" in execution_id:
144
+ raise ValueError(f"invalid audit execution id: {execution_id}")
145
+ matches = sorted(ensure_audit_home().glob(f"*/{execution_id}.json"))
146
+ if not matches:
147
+ prefix_matches = sorted(ensure_audit_home().glob(f"*/{execution_id}*.json"))
148
+ if len(prefix_matches) == 1:
149
+ return prefix_matches[0]
150
+ if len(prefix_matches) > 1:
151
+ raise ValueError(f"ambiguous audit execution id prefix: {execution_id}")
152
+ raise ValueError(f"audit execution not found: {execution_id}")
153
+ return matches[0]
154
+
155
+
156
+ def redact_value(value: Any) -> Any:
157
+ if isinstance(value, str):
158
+ return redact_secrets(value)
159
+ if isinstance(value, list):
160
+ return [redact_value(item) for item in value]
161
+ if isinstance(value, tuple):
162
+ return [redact_value(item) for item in value]
163
+ if isinstance(value, dict):
164
+ redacted: dict[str, Any] = {}
165
+ for key, item in value.items():
166
+ key_text = str(key)
167
+ if secret_key(key_text):
168
+ redacted[key_text] = "[REDACTED_SECRET]"
169
+ else:
170
+ redacted[key_text] = redact_value(item)
171
+ return redacted
172
+ return value
173
+
174
+
175
+ def secret_key(key: str) -> bool:
176
+ normalized = key.lower().replace("-", "_")
177
+ if normalized in {"token_estimate", "tokens", "prompt_tokens", "completion_tokens", "total_tokens"}:
178
+ return False
179
+ exact = {"token", "secret", "password", "passwd", "pwd", "api_key", "private_key", "pat"}
180
+ if normalized in exact:
181
+ return True
182
+ if normalized.endswith(("_token", "_secret", "_password", "_passwd", "_pwd", "_api_key", "_private_key", "_pat")):
183
+ return True
184
+ compact = re.sub(r"[^a-z0-9]+", "", key.lower())
185
+ if compact in {"tokenestimate", "tokens", "prompttokens", "completiontokens", "totaltokens"}:
186
+ return False
187
+ return any(marker in compact for marker in ("token", "secret", "password", "passwd", "apikey", "privatekey"))
188
+
189
+
190
+ def extract_prompt(args: dict[str, Any]) -> str | None:
191
+ prompt = args.get("prompt")
192
+ if isinstance(prompt, list):
193
+ return " ".join(str(item) for item in prompt).strip()
194
+ return str(prompt).strip() if prompt else None
195
+
196
+
197
+ def extract_session(result: dict[str, Any]) -> dict[str, Any] | None:
198
+ session = result.get("session")
199
+ if isinstance(session, dict):
200
+ return {
201
+ "id": session.get("id"),
202
+ "title": session.get("title"),
203
+ "project": session.get("project"),
204
+ "token_estimate": session.get("token_estimate"),
205
+ }
206
+ return None
207
+
208
+
209
+ def extract_agent(result: dict[str, Any], args: dict[str, Any]) -> dict[str, Any]:
210
+ if isinstance(result.get("agent"), dict):
211
+ return result["agent"]
212
+ if result.get("agent_id"):
213
+ return {"id": result.get("agent_id")}
214
+ if args.get("agent"):
215
+ return {"id": args.get("agent")}
216
+ route = result.get("route") if isinstance(result.get("route"), dict) else {}
217
+ if route.get("agent_id"):
218
+ return {"id": route.get("agent_id"), "capability": route.get("capability_id")}
219
+ return {}
220
+
221
+
222
+ def extract_providers(result: dict[str, Any]) -> Any:
223
+ if result.get("providers") is not None:
224
+ return result.get("providers")
225
+ route = result.get("route") if isinstance(result.get("route"), dict) else {}
226
+ provider = result.get("provider") or route.get("provider") or result.get("source_provider")
227
+ return {"used": [provider], "missing": [], "skipped": []} if provider else {"used": [], "missing": [], "skipped": []}
228
+
229
+
230
+ def extract_sources(result: dict[str, Any]) -> list[Any]:
231
+ source = result.get("source")
232
+ if source:
233
+ return [source]
234
+ return []
235
+
236
+
237
+ def extract_commands(result: dict[str, Any]) -> list[Any]:
238
+ commands = []
239
+ command = result.get("command")
240
+ if command:
241
+ commands.append(command)
242
+ for attempt in result.get("llm_backend_attempts") or []:
243
+ if isinstance(attempt, dict) and attempt.get("command"):
244
+ commands.append(attempt["command"])
245
+ return commands
246
+
247
+
248
+ def extract_permissions(result: dict[str, Any]) -> list[Any]:
249
+ permissions = []
250
+ for key in ("permission", "permissions"):
251
+ if result.get(key):
252
+ permissions.append(result[key])
253
+ preview = result.get("preview") if isinstance(result.get("preview"), dict) else {}
254
+ if preview.get("permissions"):
255
+ permissions.append(preview["permissions"])
256
+ return permissions
257
+
258
+
259
+ def extract_llm_backends(result: dict[str, Any]) -> list[Any]:
260
+ attempts = result.get("llm_backend_attempts")
261
+ if isinstance(attempts, list):
262
+ return attempts
263
+ backend = result.get("llm_backend")
264
+ return [{"id": backend}] if backend else []
265
+
266
+
267
+ def extract_token_estimate(result: dict[str, Any]) -> int | None:
268
+ session = result.get("session") if isinstance(result.get("session"), dict) else {}
269
+ if session.get("token_estimate") is not None:
270
+ return int(session.get("token_estimate") or 0)
271
+ prompt_length = result.get("prompt_length")
272
+ return int(prompt_length / 4) if isinstance(prompt_length, int) else None
273
+
274
+
275
+ def extract_external_actions(result: dict[str, Any]) -> list[dict[str, Any]]:
276
+ actions: list[dict[str, Any]] = []
277
+ preview = result.get("preview") if isinstance(result.get("preview"), dict) else {}
278
+ if preview.get("external_writes"):
279
+ actions.append({"type": preview.get("action_type"), "external_writes": True})
280
+ if result.get("external_action"):
281
+ actions.append(result["external_action"])
282
+ return actions
283
+
284
+
285
+ def summarize_result(result: dict[str, Any]) -> dict[str, Any]:
286
+ return {
287
+ "kind": result.get("kind"),
288
+ "status": result.get("status"),
289
+ "ok": result.get("ok"),
290
+ "exit_code": result.get("exit_code"),
291
+ "message": result.get("message"),
292
+ "mode": result.get("mode"),
293
+ }
294
+
295
+
296
+ def safe_user() -> str:
297
+ try:
298
+ return redact_secrets(getpass.getuser())
299
+ except Exception:
300
+ return "unknown"
301
+
302
+
303
+ def render_audit_md(entry: dict[str, Any]) -> str:
304
+ lines = [
305
+ f"# Audit {entry.get('id')}",
306
+ "",
307
+ f"- Created at: `{entry.get('created_at')}`",
308
+ f"- User: `{entry.get('user')}`",
309
+ f"- Command: `{entry.get('command')}`",
310
+ f"- Status: `{(entry.get('result') or {}).get('status')}`",
311
+ f"- OK: `{(entry.get('result') or {}).get('ok')}`",
312
+ "",
313
+ "## Prompt",
314
+ "",
315
+ entry.get("prompt") or "-",
316
+ "",
317
+ "## Agent",
318
+ "",
319
+ "```json",
320
+ json.dumps(entry.get("agent") or {}, ensure_ascii=False, indent=2, sort_keys=True),
321
+ "```",
322
+ "",
323
+ "## Providers",
324
+ "",
325
+ "```json",
326
+ json.dumps(entry.get("providers") or {}, ensure_ascii=False, indent=2, sort_keys=True),
327
+ "```",
328
+ "",
329
+ "## Permissions",
330
+ "",
331
+ "```json",
332
+ json.dumps(entry.get("permissions") or [], ensure_ascii=False, indent=2, sort_keys=True),
333
+ "```",
334
+ "",
335
+ "## Result",
336
+ "",
337
+ "```json",
338
+ json.dumps(entry.get("result") or {}, ensure_ascii=False, indent=2, sort_keys=True),
339
+ "```",
340
+ ]
341
+ if entry.get("error"):
342
+ lines.extend(["", "## Error", "", str(entry["error"])])
343
+ lines.append("")
344
+ return "\n".join(lines)
@@ -0,0 +1,163 @@
1
+ """Calendar MVP backed by local ICS files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from datetime import date, datetime, timedelta
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from cli.aikit.app_home import config_path, ensure_app_home
11
+ from cli.aikit.llm import load_config, save_config
12
+
13
+
14
+ DATE_COMPACT = "%Y%m%d"
15
+ DATETIME_COMPACT = "%Y%m%dT%H%M%S"
16
+
17
+
18
+ def configure_calendar(*, ics_path: str | None = None, timezone: str | None = None) -> dict[str, Any]:
19
+ if not ics_path:
20
+ return calendar_needs_input()
21
+ path = Path(ics_path).expanduser().resolve()
22
+ if not path.exists():
23
+ raise ValueError(f"calendar ics file not found: {ics_path}")
24
+ config = load_config()
25
+ calendar = config.setdefault("calendar", {})
26
+ calendar["provider"] = "ics"
27
+ calendar["source_ref"] = str(path)
28
+ if timezone:
29
+ calendar["timezone"] = timezone
30
+ written = save_config(config)
31
+ return {
32
+ "kind": "calendar-configure",
33
+ "status": "configured",
34
+ "provider": "ics",
35
+ "source_ref": str(path),
36
+ "timezone": timezone,
37
+ "config_path": str(written),
38
+ "stored_secret": False,
39
+ }
40
+
41
+
42
+ def calendar_today() -> dict[str, Any]:
43
+ today = date.today()
44
+ return calendar_list(today.isoformat(), today.isoformat(), label="today")
45
+
46
+
47
+ def calendar_tomorrow() -> dict[str, Any]:
48
+ tomorrow = date.today() + timedelta(days=1)
49
+ return calendar_list(tomorrow.isoformat(), tomorrow.isoformat(), label="tomorrow")
50
+
51
+
52
+ def calendar_list(date_from: str | None, date_to: str | None, *, label: str | None = None) -> dict[str, Any]:
53
+ config = load_config()
54
+ calendar = config.get("calendar") if isinstance(config.get("calendar"), dict) else {}
55
+ if calendar.get("provider") != "ics" or not calendar.get("source_ref"):
56
+ return calendar_needs_input()
57
+ start = parse_date(date_from) if date_from else date.today()
58
+ end = parse_date(date_to) if date_to else start
59
+ events = [
60
+ event
61
+ for event in parse_ics(Path(str(calendar["source_ref"])))
62
+ if event_overlaps(event, start, end)
63
+ ]
64
+ events.sort(key=lambda item: item.get("start") or "")
65
+ return {
66
+ "kind": "calendar",
67
+ "status": "ok",
68
+ "label": label,
69
+ "provider": "ics",
70
+ "source_ref": str(calendar["source_ref"]),
71
+ "from": start.isoformat(),
72
+ "to": end.isoformat(),
73
+ "events": events,
74
+ "count": len(events),
75
+ "sensitive": True,
76
+ "llm_safe": False,
77
+ }
78
+
79
+
80
+ def calendar_needs_input() -> dict[str, Any]:
81
+ ensure_app_home()
82
+ return {
83
+ "kind": "calendar",
84
+ "status": "needs-input",
85
+ "ok": False,
86
+ "requires_provider": True,
87
+ "config_path": str(config_path()),
88
+ "message": "Calendar provider is not configured.",
89
+ "next_steps": [
90
+ "Configure a local ICS file with `agent calendar configure --ics <path>`.",
91
+ "Then run `agent calendar today` or ask `agent qual minha agenda de hoje?`.",
92
+ ],
93
+ "exit_code": 2,
94
+ }
95
+
96
+
97
+ def parse_ics(path: Path) -> list[dict[str, Any]]:
98
+ text = unfold_ics(path.read_text(encoding="utf-8"))
99
+ events: list[dict[str, Any]] = []
100
+ for block in re.findall(r"BEGIN:VEVENT(.*?)END:VEVENT", text, re.DOTALL):
101
+ fields: dict[str, str] = {}
102
+ for raw_line in block.splitlines():
103
+ if ":" not in raw_line:
104
+ continue
105
+ key, value = raw_line.split(":", 1)
106
+ key = key.split(";", 1)[0].upper()
107
+ fields[key] = value.strip()
108
+ start = parse_ics_datetime(fields.get("DTSTART"))
109
+ end = parse_ics_datetime(fields.get("DTEND"))
110
+ events.append(
111
+ {
112
+ "uid": fields.get("UID"),
113
+ "summary": fields.get("SUMMARY") or "(sem titulo)",
114
+ "description": fields.get("DESCRIPTION"),
115
+ "location": fields.get("LOCATION"),
116
+ "start": start.isoformat() if start else None,
117
+ "end": end.isoformat() if end else None,
118
+ "all_day": bool(start and isinstance(start, date) and not isinstance(start, datetime)),
119
+ }
120
+ )
121
+ return events
122
+
123
+
124
+ def unfold_ics(text: str) -> str:
125
+ return re.sub(r"\r?\n[ \t]", "", text)
126
+
127
+
128
+ def parse_ics_datetime(value: str | None) -> date | datetime | None:
129
+ if not value:
130
+ return None
131
+ value = value.rstrip("Z")
132
+ for fmt in (DATETIME_COMPACT, DATE_COMPACT):
133
+ try:
134
+ parsed = datetime.strptime(value, fmt)
135
+ except ValueError:
136
+ continue
137
+ return parsed.date() if fmt == DATE_COMPACT else parsed
138
+ return None
139
+
140
+
141
+ def parse_date(value: str) -> date:
142
+ return datetime.strptime(value, "%Y-%m-%d").date()
143
+
144
+
145
+ def event_overlaps(event: dict[str, Any], start: date, end: date) -> bool:
146
+ raw_start = event.get("start")
147
+ if not raw_start:
148
+ return False
149
+ event_date = datetime.fromisoformat(raw_start).date() if "T" in raw_start else date.fromisoformat(raw_start)
150
+ return start <= event_date <= end
151
+
152
+
153
+ def calendar_summary(payload: dict[str, Any]) -> str:
154
+ if payload.get("status") != "ok":
155
+ return str(payload.get("message") or "")
156
+ if not payload.get("events"):
157
+ return "Nenhum compromisso encontrado no periodo."
158
+ lines = []
159
+ for item in payload["events"]:
160
+ start = item.get("start") or "-"
161
+ summary = item.get("summary") or "-"
162
+ lines.append(f"- {start}: {summary}")
163
+ return "\n".join(lines)
@@ -7,6 +7,7 @@ import os
7
7
  from pathlib import Path
8
8
  from typing import Any
9
9
 
10
+ from cli.aikit.app_home import APP_DIRS, app_home
10
11
  from cli.aikit.llm import config_path as llm_config_path
11
12
  from cli.aikit.llm import doctor_backends
12
13
  from cli.aikit.lock import lock_path, lock_status, read_lock
@@ -42,6 +43,14 @@ def runtime_diagnostics(
42
43
  "kind": "runtime-summary",
43
44
  "status": status,
44
45
  "root": str(root),
46
+ "app_home": str(app_home()),
47
+ "app_dirs": {
48
+ name: {
49
+ "path": str(app_home() / name),
50
+ "exists": (app_home() / name).is_dir(),
51
+ }
52
+ for name in APP_DIRS
53
+ },
45
54
  "install_home": str(install_home),
46
55
  "config_path": str(runtime_config_path()),
47
56
  "llm_config_path": str(llm_config_path()),