agent-devkit 0.0.4 → 0.1.5

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 (60) hide show
  1. package/README.md +42 -2
  2. package/package.json +1 -1
  3. package/runtime/README.md +50 -2
  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 +37 -1
  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/configuration_orchestrator.py +291 -0
  35. package/runtime/cli/aikit/decision_store.py +158 -0
  36. package/runtime/cli/aikit/diagnostics.py +9 -0
  37. package/runtime/cli/aikit/fallback.py +48 -2
  38. package/runtime/cli/aikit/github_pr.py +254 -0
  39. package/runtime/cli/aikit/identity.py +75 -0
  40. package/runtime/cli/aikit/llm.py +249 -48
  41. package/runtime/cli/aikit/main.py +1240 -32
  42. package/runtime/cli/aikit/memory.py +148 -8
  43. package/runtime/cli/aikit/model_router.py +70 -0
  44. package/runtime/cli/aikit/notifications.py +9 -0
  45. package/runtime/cli/aikit/ollama.py +237 -0
  46. package/runtime/cli/aikit/permissions.py +345 -0
  47. package/runtime/cli/aikit/personality.py +123 -0
  48. package/runtime/cli/aikit/provider_wizard.py +19 -0
  49. package/runtime/cli/aikit/providers.py +4 -5
  50. package/runtime/cli/aikit/review_gate.py +40 -0
  51. package/runtime/cli/aikit/scheduler.py +18 -0
  52. package/runtime/cli/aikit/sessions.py +396 -0
  53. package/runtime/cli/aikit/setup_wizard.py +12 -0
  54. package/runtime/cli/aikit/tasks.py +351 -0
  55. package/runtime/cli/aikit/toolchain.py +274 -0
  56. package/runtime/providers/calendar.yaml +28 -0
  57. package/runtime/providers/github.yaml +42 -0
  58. package/runtime/providers/local-notification.yaml +19 -0
  59. package/runtime/providers/local-scheduler.yaml +24 -0
  60. package/runtime/vendor/skills/napkin/napkin.md +13 -3
@@ -12,19 +12,54 @@ import sys
12
12
  from pathlib import Path
13
13
  from typing import Any
14
14
 
15
+ from cli.aikit.aliases import add_alias, list_aliases, remove_alias, sync_aliases
15
16
  from cli.aikit import __version__
17
+ from cli.aikit.audit import export_audit, list_audits, record_audit, show_audit
18
+ from cli.aikit.calendar import calendar_list, calendar_summary, calendar_today, calendar_tomorrow, configure_calendar
19
+ from cli.aikit.configuration_orchestrator import provider_wizard_from_requirement
20
+ from cli.aikit.decision_store import list_decisions, reset_decisions, set_decision
16
21
  from cli.aikit.diagnostics import build_diagnostics
17
22
  from cli.aikit.fallback import evaluate_provider_requirements
23
+ from cli.aikit.github_pr import pr_create_automation, pr_inspect, pr_list_review_requests, pr_review
18
24
  from cli.aikit.guardrails import evaluate_execution_guardrails
25
+ from cli.aikit.identity import enforce_identity_response, is_identity_question, local_identity_response, public_name
19
26
  from cli.aikit.llm import (
20
27
  configure_backend,
21
28
  doctor_backends,
22
29
  invoke_agent_prompt,
23
30
  list_backends,
24
- resolve_backend,
31
+ llm_preference,
25
32
  set_default_backend,
33
+ set_llm_preference,
26
34
  )
27
- from cli.aikit.memory import record_usage, reset_memory, show_memory, napkin_context
35
+ from cli.aikit.memory import memory_path_payload, napkin_context, record_usage, reset_memory, show_memory
36
+ from cli.aikit.model_router import build_model_plan
37
+ from cli.aikit.ollama import ollama_models, ollama_pull, ollama_status, ollama_update
38
+ from cli.aikit.personality import load_personality, reset_personality, setup_personality, update_personality
39
+ from cli.aikit.permissions import grant_permission, revoke_permission, show_permissions
40
+ from cli.aikit.provider_wizard import missing_source_wizard
41
+ from cli.aikit.review_gate import build_review_gate, mark_reviewed
42
+ from cli.aikit.sessions import (
43
+ build_contextual_prompt,
44
+ get_or_create_session,
45
+ list_sessions,
46
+ record_exchange,
47
+ resume_session,
48
+ show_session,
49
+ )
50
+ from cli.aikit.setup_wizard import setup_wizard
51
+ from cli.aikit.scheduler import run_scheduler_once, scheduler_daemon_plan
52
+ from cli.aikit.tasks import (
53
+ create_task,
54
+ delete_task,
55
+ list_tasks,
56
+ run_task,
57
+ show_task,
58
+ task_history,
59
+ update_task_schedule,
60
+ update_task_status,
61
+ )
62
+ from cli.aikit.toolchain import doctor_toolchain, install_toolchain, list_toolchain
28
63
  from cli.aikit.install import InstallError, install_runtime
29
64
  from cli.aikit.lock import lock_status, parse_profiles
30
65
  from cli.aikit.output import run_payload
@@ -69,6 +104,23 @@ DETERMINISTIC_COMMANDS = (
69
104
  "credential",
70
105
  "source",
71
106
  "memory",
107
+ "personality",
108
+ "setup",
109
+ "alias",
110
+ "session",
111
+ "toolchain",
112
+ "task",
113
+ "scheduler",
114
+ "calendar",
115
+ "pr",
116
+ "permissions",
117
+ "audit",
118
+ "config",
119
+ "tools",
120
+ "integrations",
121
+ "skills",
122
+ "decisions",
123
+ "ollama",
72
124
  "install",
73
125
  )
74
126
  LLM_COMMANDS = ("agent",)
@@ -86,11 +138,13 @@ def main(argv: list[str] | None = None, *, prog: str | None = None) -> int:
86
138
  try:
87
139
  result = dispatch(args)
88
140
  except DevKitError as exc:
141
+ maybe_record_cli_audit(args, result=None, error=str(exc))
89
142
  print(f"error: {exc}", file=sys.stderr)
90
143
  return 1
91
144
 
92
145
  if result is None:
93
146
  return 0
147
+ maybe_record_cli_audit(args, result=result, error=None)
94
148
 
95
149
  if getattr(args, "json", False):
96
150
  print(json.dumps(result, ensure_ascii=False, indent=2))
@@ -109,7 +163,15 @@ def build_parser(prog: str | None = None) -> argparse.ArgumentParser:
109
163
  description="AI DevKit CLI",
110
164
  )
111
165
  parser.add_argument("--json", action="store_true", help="print machine-readable JSON")
166
+ parser.add_argument("--dry-run", dest="global_dry_run", action="store_true", help="show execution plan without external write effects")
112
167
  parser.add_argument("-v", "--version", action="store_true", help="print CLI version and exit")
168
+ parser.add_argument(
169
+ "-s",
170
+ "--sessions",
171
+ dest="sessions_shortcut",
172
+ action="store_true",
173
+ help="list local conversation sessions and exit",
174
+ )
113
175
 
114
176
  subparsers = parser.add_subparsers(dest="command")
115
177
 
@@ -119,6 +181,10 @@ def build_parser(prog: str | None = None) -> argparse.ArgumentParser:
119
181
  )
120
182
  agent_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
121
183
  agent_parser.add_argument("--llm", help="LLM backend id to use")
184
+ agent_parser.add_argument("--dry-run", action="store_true", help="show execution plan without invoking LLM or external writes")
185
+ agent_parser.add_argument("--no-llm-fallback", action="store_true", help="disable automatic fallback to secondary LLM backends")
186
+ agent_parser.add_argument("--session", dest="session_id", help="resume a local conversation session")
187
+ agent_parser.add_argument("--new-session", action="store_true", help="start a new local conversation session")
122
188
  agent_parser.add_argument("prompt", nargs=argparse.REMAINDER)
123
189
 
124
190
  commands_parser = subparsers.add_parser("commands", help="list CLI command modes")
@@ -159,10 +225,125 @@ def build_parser(prog: str | None = None) -> argparse.ArgumentParser:
159
225
 
160
226
  memory_parser = subparsers.add_parser("memory", help="inspect or reset local AI DevKit memory")
161
227
  memory_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
162
- memory_parser.add_argument("action", nargs="?", default="show", choices=["show", "reset"])
228
+ memory_parser.add_argument("action", nargs="?", default="show", choices=["show", "path", "reset"])
163
229
  memory_parser.add_argument("--agent", dest="agent_id")
164
230
  memory_parser.add_argument("--source", dest="source_id")
165
231
  memory_parser.add_argument("--all", action="store_true", help="reset all local memory")
232
+ memory_parser.add_argument("--sessions", action="store_true", help="reset local conversation sessions")
233
+ memory_parser.add_argument("--tasks", action="store_true", help="reset local task schedules")
234
+ memory_parser.add_argument("--cache", action="store_true", help="reset local cache")
235
+
236
+ personality_parser = subparsers.add_parser("personality", help="inspect or update local Agent DevKit personality")
237
+ personality_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
238
+ personality_parser.add_argument("action", nargs="?", default="show", choices=["show", "edit", "reset"])
239
+ personality_parser.add_argument("--name", dest="agent_name", help="public agent name")
240
+ personality_parser.add_argument("--user-name", help="user name")
241
+ personality_parser.add_argument("--language", help="default response language")
242
+ personality_parser.add_argument("--tone", help="response tone")
243
+ personality_parser.add_argument("--detail-level", help="response detail level")
244
+
245
+ setup_parser = subparsers.add_parser("setup", help="run setup helpers")
246
+ setup_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
247
+ setup_parser.add_argument("--dry-run", action="store_true", help="show setup plan without installing external tools")
248
+ setup_parser.add_argument("--yes", action="store_true", help="confirm setup actions")
249
+ setup_parser.add_argument("action", nargs="?", default="plan", choices=["plan", "personality"])
250
+
251
+ alias_parser = subparsers.add_parser("alias", help="manage local command aliases for agent")
252
+ alias_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
253
+ alias_parser.add_argument("action", nargs="?", default="list", choices=["add", "list", "remove", "sync"])
254
+ alias_parser.add_argument("name", nargs="?")
255
+ alias_parser.add_argument("--force", action="store_true", help="allow replacing an existing local alias file")
256
+
257
+ session_parser = subparsers.add_parser("session", help="manage local conversation sessions")
258
+ session_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
259
+ session_parser.add_argument("action", nargs="?", default="list", choices=["list", "show", "resume"])
260
+ session_parser.add_argument("session_id", nargs="?")
261
+
262
+ toolchain_parser = subparsers.add_parser("toolchain", help="inspect or install external tools")
263
+ toolchain_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
264
+ toolchain_parser.add_argument("action", nargs="?", default="list", choices=["list", "doctor", "install"])
265
+ toolchain_parser.add_argument("tool", nargs="?", default="all")
266
+ toolchain_parser.add_argument("--dry-run", action="store_true", help="show install plan without executing it")
267
+ toolchain_parser.add_argument("--yes", action="store_true", help="confirm external tool installation")
268
+
269
+ task_parser = subparsers.add_parser("task", help="manage local scheduled tasks")
270
+ task_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
271
+ task_parser.add_argument("action", nargs="?", default="list", choices=["create", "list", "show", "history", "run", "pause", "resume", "update", "enable", "disable", "delete"])
272
+ task_parser.add_argument("task_id", nargs="?")
273
+ task_parser.add_argument("--title")
274
+ task_parser.add_argument("--prompt")
275
+ task_parser.add_argument("--every")
276
+ task_parser.add_argument("--cron")
277
+ task_parser.add_argument("--dry-run", action="store_true")
278
+ task_parser.add_argument("--yes", action="store_true")
279
+
280
+ scheduler_parser = subparsers.add_parser("scheduler", help="run local scheduler")
281
+ scheduler_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
282
+ scheduler_parser.add_argument("action", nargs="?", default="run-once", choices=["run-once", "daemon"])
283
+ scheduler_parser.add_argument("--dry-run", action="store_true")
284
+
285
+ calendar_parser = subparsers.add_parser("calendar", help="inspect configured calendar")
286
+ calendar_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
287
+ calendar_parser.add_argument("action", nargs="?", default="today", choices=["today", "tomorrow", "list", "configure"])
288
+ calendar_parser.add_argument("--from", dest="date_from")
289
+ calendar_parser.add_argument("--to", dest="date_to")
290
+ calendar_parser.add_argument("--ics")
291
+ calendar_parser.add_argument("--timezone")
292
+
293
+ pr_parser = subparsers.add_parser("pr", help="review GitHub pull requests")
294
+ pr_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
295
+ pr_parser.add_argument("action", nargs="?", default="list-review-requests", choices=["list-review-requests", "inspect", "review", "automation"])
296
+ pr_parser.add_argument("pr_ref", nargs="?")
297
+ pr_parser.add_argument("automation_action", nargs="?")
298
+ pr_parser.add_argument("--approve", action="store_true")
299
+ pr_parser.add_argument("--request-changes", action="store_true")
300
+ pr_parser.add_argument("--comment")
301
+ pr_parser.add_argument("--allow-write", action="store_true")
302
+ pr_parser.add_argument("--dry-run", action="store_true")
303
+ pr_parser.add_argument("--time", default="09:00")
304
+
305
+ permissions_parser = subparsers.add_parser("permissions", help="manage local permission policies")
306
+ permissions_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
307
+ permissions_parser.add_argument("action", nargs="?", default="show", choices=["show", "grant", "revoke"])
308
+ permissions_parser.add_argument("agent", nargs="?")
309
+ permissions_parser.add_argument("provider", nargs="?")
310
+ permissions_parser.add_argument("level", nargs="?")
311
+ permissions_parser.add_argument("--project")
312
+ permissions_parser.add_argument("--task", dest="task_id")
313
+
314
+ audit_parser = subparsers.add_parser("audit", help="inspect local execution audit trail")
315
+ audit_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
316
+ audit_parser.add_argument("action", nargs="?", default="list", choices=["list", "show", "export"])
317
+ audit_parser.add_argument("execution_id", nargs="?")
318
+ audit_parser.add_argument("--limit", type=int, default=20)
319
+ audit_parser.add_argument("--format", default="md", choices=["md", "json"])
320
+
321
+ config_parser = subparsers.add_parser("config", help="inspect local Agent DevKit configuration")
322
+ config_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
323
+ config_parser.add_argument("action", nargs="?", default="show", choices=["show", "path"])
324
+
325
+ for command_name, help_text in (
326
+ ("tools", "manage enabled local tools"),
327
+ ("integrations", "manage provider integration decisions"),
328
+ ("skills", "manage local skill decisions"),
329
+ ):
330
+ control_parser = subparsers.add_parser(command_name, help=help_text)
331
+ control_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
332
+ control_parser.add_argument("action", nargs="?", default="list", choices=["list", "enable", "disable"])
333
+ control_parser.add_argument("item_id", nargs="?")
334
+
335
+ decisions_parser = subparsers.add_parser("decisions", help="inspect or reset local opt-in and opt-out decisions")
336
+ decisions_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
337
+ decisions_parser.add_argument("action", nargs="?", default="list", choices=["list", "forget", "reset"])
338
+ decisions_parser.add_argument("item_id", nargs="?")
339
+ decisions_parser.add_argument("--category", choices=["tools", "integrations", "skills", "llms"])
340
+
341
+ ollama_parser = subparsers.add_parser("ollama", help="inspect and manage local Ollama models")
342
+ ollama_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
343
+ ollama_parser.add_argument("action", nargs="?", default="status", choices=["status", "models", "pull", "update"])
344
+ ollama_parser.add_argument("model", nargs="?")
345
+ ollama_parser.add_argument("--yes", action="store_true", help="confirm Ollama model or update operation")
346
+ ollama_parser.add_argument("--dry-run", action="store_true", help="show Ollama operation without executing it")
166
347
 
167
348
  llm_parser = subparsers.add_parser("llm", help="manage LLM backends")
168
349
  llm_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
@@ -170,14 +351,17 @@ def build_parser(prog: str | None = None) -> argparse.ArgumentParser:
170
351
  "action",
171
352
  nargs="?",
172
353
  default="list",
173
- choices=["list", "doctor", "configure", "set-default"],
354
+ choices=["list", "doctor", "configure", "set-default", "default", "enable", "disable", "preference"],
174
355
  )
175
356
  llm_parser.add_argument("backend", nargs="?")
357
+ llm_parser.add_argument("preference_value", nargs="?")
176
358
  llm_parser.add_argument("--api-key-env", help="environment variable that stores the API key")
177
359
  llm_parser.add_argument("--base-url", help="OpenAI-compatible base URL")
178
360
  llm_parser.add_argument("--model", help="default model id")
179
361
  llm_parser.add_argument("--command", dest="host_command", help="host CLI command name or path")
180
362
  llm_parser.add_argument("--set-default", action="store_true", help="set backend as the default LLM")
363
+ llm_parser.add_argument("--primary", help="primary backend for LLM preference")
364
+ llm_parser.add_argument("--order", help="comma-separated fallback order")
181
365
 
182
366
  install_parser = subparsers.add_parser("install", help="install AI DevKit host artifacts")
183
367
  install_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
@@ -237,6 +421,8 @@ def build_parser(prog: str | None = None) -> argparse.ArgumentParser:
237
421
  def dispatch(args: argparse.Namespace) -> dict[str, Any] | None:
238
422
  if args.version:
239
423
  return {"kind": "version", "program": getattr(args, "prog_name", "aikit"), "version": __version__}
424
+ if args.sessions_shortcut:
425
+ return list_sessions()
240
426
 
241
427
  if not args.command:
242
428
  raise DevKitError("missing command. Use --help for usage.")
@@ -256,6 +442,36 @@ def dispatch(args: argparse.Namespace) -> dict[str, Any] | None:
256
442
  return dispatch_source(args)
257
443
  if command == "memory":
258
444
  return dispatch_memory(args)
445
+ if command == "personality":
446
+ return dispatch_personality(args)
447
+ if command == "setup":
448
+ return dispatch_setup(args)
449
+ if command == "alias":
450
+ return dispatch_alias(args)
451
+ if command == "session":
452
+ return dispatch_session(args)
453
+ if command == "toolchain":
454
+ return dispatch_toolchain(args)
455
+ if command == "task":
456
+ return dispatch_task(args)
457
+ if command == "scheduler":
458
+ return dispatch_scheduler(args)
459
+ if command == "calendar":
460
+ return dispatch_calendar(args)
461
+ if command == "pr":
462
+ return dispatch_pr(args)
463
+ if command == "permissions":
464
+ return dispatch_permissions(args)
465
+ if command == "audit":
466
+ return dispatch_audit(args)
467
+ if command == "config":
468
+ return dispatch_config(args)
469
+ if command in {"tools", "integrations", "skills"}:
470
+ return dispatch_control_category(command, args)
471
+ if command == "decisions":
472
+ return dispatch_decisions(args)
473
+ if command == "ollama":
474
+ return dispatch_ollama(args)
259
475
  if command == "llm":
260
476
  return dispatch_llm(args)
261
477
  if command == "install":
@@ -327,8 +543,22 @@ def list_command_modes() -> dict[str, Any]:
327
543
  }
328
544
 
329
545
 
546
+ def maybe_record_cli_audit(args: argparse.Namespace, *, result: dict[str, Any] | None, error: str | None) -> None:
547
+ command = canonical_command(getattr(args, "command", None) or "")
548
+ if command in {"", "audit"} or getattr(args, "version", False):
549
+ return
550
+ try:
551
+ audit = record_audit(command=command, args=vars(args), result=result, error=error)
552
+ except Exception:
553
+ return
554
+ if result is not None:
555
+ result["audit"] = audit
556
+
557
+
330
558
  def dispatch_llm(args: argparse.Namespace) -> dict[str, Any]:
331
559
  try:
560
+ if args.action != "preference" and args.preference_value:
561
+ raise DevKitError(f"llm {args.action} received an unexpected argument: {args.preference_value}")
332
562
  if args.action == "list":
333
563
  if args.backend:
334
564
  raise DevKitError("llm list does not accept a backend argument")
@@ -346,15 +576,112 @@ def dispatch_llm(args: argparse.Namespace) -> dict[str, Any]:
346
576
  command=args.host_command,
347
577
  set_default=args.set_default,
348
578
  )
349
- if args.action == "set-default":
579
+ if args.action in {"set-default", "default"}:
350
580
  if not args.backend:
351
- raise DevKitError("llm set-default requires a backend")
581
+ raise DevKitError(f"llm {args.action} requires a backend")
352
582
  return set_default_backend(args.backend)
583
+ if args.action in {"enable", "disable"}:
584
+ if not args.backend:
585
+ raise DevKitError(f"llm {args.action} requires a backend")
586
+ state = "enabled" if args.action == "enable" else "disabled_by_user"
587
+ return set_decision("llms", args.backend, state, reason=f"llm {args.action} command")
588
+ if args.action == "preference":
589
+ if args.backend in {None, "show"}:
590
+ return llm_preference()
591
+ if args.backend == "set":
592
+ if not args.primary and not args.order:
593
+ raise DevKitError("llm preference set requires --primary or --order")
594
+ return set_llm_preference(primary=args.primary, order=args.order)
595
+ if args.backend == "reorder":
596
+ order = args.preference_value or args.order
597
+ if not order:
598
+ raise DevKitError("llm preference reorder requires an order value or --order")
599
+ return set_llm_preference(order=order)
600
+ raise DevKitError("llm preference action must be show, set or reorder")
353
601
  except ValueError as exc:
354
602
  raise DevKitError(str(exc)) from exc
355
603
  raise DevKitError(f"unsupported llm action: {args.action}")
356
604
 
357
605
 
606
+ def dispatch_config(args: argparse.Namespace) -> dict[str, Any]:
607
+ from cli.aikit.llm import config_path
608
+
609
+ if args.action == "path":
610
+ return {"kind": "config", "status": "ok", "path": str(config_path())}
611
+ if args.action == "show":
612
+ return {
613
+ "kind": "config",
614
+ "status": "ok",
615
+ "path": str(config_path()),
616
+ "decisions": list_decisions(),
617
+ "llm": llm_preference(),
618
+ "ollama": ollama_status(),
619
+ }
620
+ raise DevKitError(f"unsupported config action: {args.action}")
621
+
622
+
623
+ def dispatch_control_category(command: str, args: argparse.Namespace) -> dict[str, Any]:
624
+ try:
625
+ category = command
626
+ if args.action == "list":
627
+ if args.item_id:
628
+ raise DevKitError(f"{command} list does not accept an item id")
629
+ payload = list_decisions(category)
630
+ payload["kind"] = command
631
+ return payload
632
+ if args.action in {"enable", "disable"}:
633
+ if not args.item_id:
634
+ raise DevKitError(f"{command} {args.action} requires an item id")
635
+ state = "enabled" if args.action == "enable" else "disabled_by_user"
636
+ payload = set_decision(category, args.item_id, state, reason=f"{command} {args.action} command")
637
+ payload["kind"] = command[:-1] if command.endswith("s") else command
638
+ return payload
639
+ except ValueError as exc:
640
+ raise DevKitError(str(exc)) from exc
641
+ raise DevKitError(f"unsupported {command} action: {args.action}")
642
+
643
+
644
+ def dispatch_decisions(args: argparse.Namespace) -> dict[str, Any]:
645
+ try:
646
+ if args.action == "list":
647
+ if args.item_id:
648
+ raise DevKitError("decisions list does not accept an item id")
649
+ return list_decisions(args.category)
650
+ if args.action == "reset":
651
+ if args.item_id:
652
+ raise DevKitError("decisions reset does not accept an item id")
653
+ return reset_decisions(args.category)
654
+ if args.action == "forget":
655
+ if not args.item_id:
656
+ raise DevKitError("decisions forget requires an item id")
657
+ category = args.category or "tools"
658
+ return set_decision(category, args.item_id, "available", reason="decision forgotten")
659
+ except ValueError as exc:
660
+ raise DevKitError(str(exc)) from exc
661
+ raise DevKitError(f"unsupported decisions action: {args.action}")
662
+
663
+
664
+ def dispatch_ollama(args: argparse.Namespace) -> dict[str, Any]:
665
+ try:
666
+ if args.action == "status":
667
+ if args.model:
668
+ raise DevKitError("ollama status does not accept a model")
669
+ return ollama_status()
670
+ if args.action == "models":
671
+ if args.model:
672
+ raise DevKitError("ollama models does not accept a model")
673
+ return ollama_models()
674
+ if args.action == "pull":
675
+ return ollama_pull(args.model, yes=args.yes, dry_run=effective_dry_run(args))
676
+ if args.action == "update":
677
+ if args.model:
678
+ raise DevKitError("ollama update does not accept a model")
679
+ return ollama_update(yes=args.yes, dry_run=effective_dry_run(args))
680
+ except ValueError as exc:
681
+ raise DevKitError(str(exc)) from exc
682
+ raise DevKitError(f"unsupported ollama action: {args.action}")
683
+
684
+
358
685
  def dispatch_install(args: argparse.Namespace) -> dict[str, Any]:
359
686
  try:
360
687
  return install_runtime(
@@ -363,7 +690,7 @@ def dispatch_install(args: argparse.Namespace) -> dict[str, Any]:
363
690
  host=args.host,
364
691
  target=Path(args.target) if args.target else None,
365
692
  home=Path(args.home) if args.home else None,
366
- dry_run=args.dry_run,
693
+ dry_run=effective_dry_run(args),
367
694
  profiles=parse_profiles(args.profiles),
368
695
  )
369
696
  except InstallError as exc:
@@ -452,40 +779,549 @@ def dispatch_source(args: argparse.Namespace) -> dict[str, Any]:
452
779
  def dispatch_memory(args: argparse.Namespace) -> dict[str, Any]:
453
780
  if args.action == "show":
454
781
  return show_memory(ROOT, agent_id=args.agent_id, source_id=args.source_id)
782
+ if args.action == "path":
783
+ return memory_path_payload()
455
784
  if args.action == "reset":
456
- return reset_memory(all_memory=args.all, agent_id=args.agent_id, source_id=args.source_id)
785
+ return reset_memory(
786
+ all_memory=args.all,
787
+ agent_id=args.agent_id,
788
+ source_id=args.source_id,
789
+ reset_sessions=args.sessions,
790
+ reset_tasks=args.tasks,
791
+ reset_cache=args.cache,
792
+ )
457
793
  raise DevKitError(f"unsupported memory action: {args.action}")
458
794
 
459
795
 
796
+ def dispatch_personality(args: argparse.Namespace) -> dict[str, Any]:
797
+ if args.action == "show":
798
+ return load_personality()
799
+ if args.action == "edit":
800
+ if not any([args.agent_name, args.user_name, args.language, args.tone, args.detail_level]):
801
+ payload = load_personality()
802
+ payload["status"] = "needs-input"
803
+ payload["message"] = "Use --name, --user-name, --language, --tone or --detail-level to edit non-interactively."
804
+ return payload
805
+ return update_personality(
806
+ agent_name=args.agent_name,
807
+ user_name=args.user_name,
808
+ language=args.language,
809
+ tone=args.tone,
810
+ detail_level=args.detail_level,
811
+ )
812
+ if args.action == "reset":
813
+ return reset_personality()
814
+ raise DevKitError(f"unsupported personality action: {args.action}")
815
+
816
+
817
+ def dispatch_setup(args: argparse.Namespace) -> dict[str, Any]:
818
+ if args.action == "personality":
819
+ return setup_personality()
820
+ if args.action == "plan":
821
+ return setup_wizard(ROOT, dry_run=effective_dry_run(args), yes=args.yes)
822
+ raise DevKitError(f"unsupported setup action: {args.action}")
823
+
824
+
825
+ def dispatch_toolchain(args: argparse.Namespace) -> dict[str, Any]:
826
+ try:
827
+ if args.action == "list":
828
+ if args.tool != "all":
829
+ raise DevKitError("toolchain list does not accept a tool argument")
830
+ return list_toolchain(ROOT)
831
+ if args.action == "doctor":
832
+ return doctor_toolchain(ROOT, None if args.tool == "all" else args.tool)
833
+ if args.action == "install":
834
+ return install_toolchain(ROOT, None if args.tool == "all" else args.tool, dry_run=effective_dry_run(args), yes=args.yes)
835
+ except ValueError as exc:
836
+ raise DevKitError(str(exc)) from exc
837
+ raise DevKitError(f"unsupported toolchain action: {args.action}")
838
+
839
+
840
+ def dispatch_task(args: argparse.Namespace) -> dict[str, Any]:
841
+ try:
842
+ if args.action == "list":
843
+ return list_tasks()
844
+ if args.action == "create":
845
+ schedule = {"type": "manual"}
846
+ if args.every:
847
+ schedule = {"type": "interval", "every": args.every}
848
+ if args.cron:
849
+ schedule = {"type": "cron", "cron": args.cron}
850
+ return create_task(task_id=args.task_id, title=args.title, prompt=args.prompt, schedule=schedule)
851
+ if args.action == "show":
852
+ require_id(args.task_id, "task show")
853
+ return show_task(args.task_id)
854
+ if args.action == "history":
855
+ require_id(args.task_id, "task history")
856
+ return task_history(args.task_id)
857
+ if args.action == "run":
858
+ require_id(args.task_id, "task run")
859
+ return run_task(args.task_id, dry_run=effective_dry_run(args))
860
+ if args.action in {"pause", "disable"}:
861
+ require_id(args.task_id, f"task {args.action}")
862
+ return update_task_status(args.task_id, "paused" if args.action == "pause" else "disabled")
863
+ if args.action in {"resume", "enable"}:
864
+ require_id(args.task_id, f"task {args.action}")
865
+ return update_task_status(args.task_id, "enabled")
866
+ if args.action == "update":
867
+ require_id(args.task_id, "task update")
868
+ return update_task_schedule(args.task_id, every=args.every, cron=args.cron)
869
+ if args.action == "delete":
870
+ require_id(args.task_id, "task delete")
871
+ return delete_task(args.task_id, yes=args.yes)
872
+ except ValueError as exc:
873
+ raise DevKitError(str(exc)) from exc
874
+ raise DevKitError(f"unsupported task action: {args.action}")
875
+
876
+
877
+ def dispatch_scheduler(args: argparse.Namespace) -> dict[str, Any]:
878
+ if args.action == "run-once":
879
+ return run_scheduler_once(dry_run=effective_dry_run(args))
880
+ if args.action == "daemon":
881
+ return scheduler_daemon_plan()
882
+ raise DevKitError(f"unsupported scheduler action: {args.action}")
883
+
884
+
885
+ def dispatch_calendar(args: argparse.Namespace) -> dict[str, Any]:
886
+ try:
887
+ if args.action == "configure":
888
+ return configure_calendar(ics_path=args.ics, timezone=args.timezone)
889
+ if args.action == "today":
890
+ return calendar_today()
891
+ if args.action == "tomorrow":
892
+ return calendar_tomorrow()
893
+ if args.action == "list":
894
+ return calendar_list(args.date_from, args.date_to)
895
+ except ValueError as exc:
896
+ raise DevKitError(str(exc)) from exc
897
+ raise DevKitError(f"unsupported calendar action: {args.action}")
898
+
899
+
900
+ def dispatch_pr(args: argparse.Namespace) -> dict[str, Any]:
901
+ if args.action == "list-review-requests":
902
+ if effective_dry_run(args):
903
+ return pr_read_dry_run("list-review-requests")
904
+ return pr_list_review_requests()
905
+ if args.action == "inspect":
906
+ require_id(args.pr_ref, "pr inspect")
907
+ if effective_dry_run(args):
908
+ return pr_read_dry_run("inspect", pr_ref=args.pr_ref)
909
+ return pr_inspect(args.pr_ref)
910
+ if args.action == "review":
911
+ require_id(args.pr_ref, "pr review")
912
+ return pr_review(
913
+ args.pr_ref,
914
+ approve=args.approve,
915
+ request_changes=args.request_changes,
916
+ comment=args.comment,
917
+ allow_write=args.allow_write,
918
+ dry_run=effective_dry_run(args),
919
+ )
920
+ if args.action == "automation":
921
+ if args.automation_action not in {None, "create"}:
922
+ raise DevKitError("pr automation action must be create")
923
+ if effective_dry_run(args):
924
+ return pr_automation_dry_run(time=args.time)
925
+ return pr_create_automation(time=args.time)
926
+ raise DevKitError(f"unsupported pr action: {args.action}")
927
+
928
+
929
+ def dispatch_permissions(args: argparse.Namespace) -> dict[str, Any]:
930
+ try:
931
+ if args.action == "show":
932
+ return show_permissions()
933
+ if args.action == "grant":
934
+ if not args.agent or not args.provider or not args.level:
935
+ raise DevKitError("permissions grant requires agent, provider and level")
936
+ return grant_permission(args.agent, args.provider, args.level, project=args.project, task_id=args.task_id)
937
+ if args.action == "revoke":
938
+ if not args.agent or not args.provider:
939
+ raise DevKitError("permissions revoke requires agent and provider")
940
+ return revoke_permission(args.agent, args.provider, args.level, project=args.project, task_id=args.task_id)
941
+ except ValueError as exc:
942
+ raise DevKitError(str(exc)) from exc
943
+ raise DevKitError(f"unsupported permissions action: {args.action}")
944
+
945
+
946
+ def dispatch_audit(args: argparse.Namespace) -> dict[str, Any]:
947
+ try:
948
+ if args.action == "list":
949
+ if args.execution_id:
950
+ raise DevKitError("audit list does not accept an execution id")
951
+ return list_audits(limit=max(1, int(args.limit or 20)))
952
+ if args.action == "show":
953
+ require_id(args.execution_id, "audit show")
954
+ return show_audit(args.execution_id)
955
+ if args.action == "export":
956
+ require_id(args.execution_id, "audit export")
957
+ return export_audit(args.execution_id, fmt=args.format)
958
+ except ValueError as exc:
959
+ raise DevKitError(str(exc)) from exc
960
+ raise DevKitError(f"unsupported audit action: {args.action}")
961
+
962
+
963
+ def pr_read_dry_run(action: str, *, pr_ref: str | None = None) -> dict[str, Any]:
964
+ return {
965
+ "kind": "pr",
966
+ "status": "planned",
967
+ "ok": True,
968
+ "dry_run": True,
969
+ "mode": "report-only",
970
+ "provider": "github",
971
+ "action": action,
972
+ "pr_ref": pr_ref,
973
+ "commands": planned_pr_commands(action, pr_ref=pr_ref),
974
+ "summary": "Dry-run only. GitHub would be read through gh; no PR write action would be submitted.",
975
+ }
976
+
977
+
978
+ def pr_automation_dry_run(*, time: str) -> dict[str, Any]:
979
+ return {
980
+ "kind": "pr-automation",
981
+ "status": "planned",
982
+ "ok": True,
983
+ "dry_run": True,
984
+ "mode": "report-only",
985
+ "provider": "github",
986
+ "task": {
987
+ "id": "daily-pr-review",
988
+ "title": "Revisar PRs pendentes diariamente",
989
+ "schedule": {"type": "daily", "time": time},
990
+ "action": {
991
+ "type": "capability",
992
+ "agent": "github-pr-reviewer",
993
+ "capability": "list-review-requests",
994
+ "external_writes": False,
995
+ },
996
+ "permissions": {"mode": "report-only", "comment": False, "approve": False, "request_changes": False},
997
+ },
998
+ "summary": "Dry-run only. A local report-only PR review task would be created.",
999
+ }
1000
+
1001
+
1002
+ def planned_pr_commands(action: str, *, pr_ref: str | None = None) -> list[list[str]]:
1003
+ if action == "list-review-requests":
1004
+ return [["gh", "pr", "list", "--review-requested", "@me", "--json", "number,title,url,author,headRefName,baseRefName,isDraft"]]
1005
+ if action == "inspect":
1006
+ return [["gh", "pr", "view", str(pr_ref), "--json", "number,title,url,author,body,headRefName,baseRefName,state,isDraft,reviewDecision,mergeable"]]
1007
+ return []
1008
+
1009
+
1010
+ def require_id(value: str | None, command: str) -> None:
1011
+ if not value:
1012
+ raise DevKitError(f"{command} requires an id")
1013
+
1014
+
1015
+ def effective_dry_run(args: argparse.Namespace) -> bool:
1016
+ return bool(getattr(args, "dry_run", False) or getattr(args, "global_dry_run", False))
1017
+
1018
+
1019
+ def dispatch_alias(args: argparse.Namespace) -> dict[str, Any]:
1020
+ try:
1021
+ if args.action == "list":
1022
+ if args.name:
1023
+ raise DevKitError("alias list does not accept a name")
1024
+ return list_aliases()
1025
+ if args.action == "add":
1026
+ if not args.name:
1027
+ raise DevKitError("alias add requires a name")
1028
+ return add_alias(args.name, force=args.force)
1029
+ if args.action == "remove":
1030
+ if not args.name:
1031
+ raise DevKitError("alias remove requires a name")
1032
+ return remove_alias(args.name)
1033
+ if args.action == "sync":
1034
+ if args.name:
1035
+ raise DevKitError("alias sync does not accept a name")
1036
+ return sync_aliases()
1037
+ except ValueError as exc:
1038
+ raise DevKitError(str(exc)) from exc
1039
+ raise DevKitError(f"unsupported alias action: {args.action}")
1040
+
1041
+
1042
+ def dispatch_session(args: argparse.Namespace) -> dict[str, Any]:
1043
+ try:
1044
+ if args.action == "list":
1045
+ if args.session_id:
1046
+ raise DevKitError("session list does not accept a session id")
1047
+ return list_sessions()
1048
+ if args.action == "show":
1049
+ if not args.session_id:
1050
+ raise DevKitError("session show requires a session id")
1051
+ return show_session(args.session_id)
1052
+ if args.action == "resume":
1053
+ if not args.session_id:
1054
+ raise DevKitError("session resume requires a session id")
1055
+ return resume_session(args.session_id)
1056
+ except ValueError as exc:
1057
+ raise DevKitError(str(exc)) from exc
1058
+ raise DevKitError(f"unsupported session action: {args.action}")
1059
+
1060
+
460
1061
  def agent_requires_llm(args: argparse.Namespace) -> dict[str, Any]:
461
1062
  prompt = " ".join(args.prompt).strip()
462
1063
  if not prompt:
463
1064
  raise DevKitError("agent requires a natural-language prompt")
464
- route = route_prompt(prompt)
465
- if route:
466
- return invoke_deterministic_route(prompt, route)
1065
+ if effective_dry_run(args):
1066
+ return build_agent_dry_run_plan(prompt, args)
467
1067
  try:
468
- backend = resolve_backend(args.llm)
1068
+ session = get_or_create_session(
1069
+ session_id=args.session_id,
1070
+ force_new=args.new_session,
1071
+ prompt=prompt,
1072
+ project=str(Path.cwd()),
1073
+ backend=args.llm,
1074
+ )
469
1075
  except ValueError as exc:
470
1076
  raise DevKitError(str(exc)) from exc
471
- if backend and backend.get("status") == "ok":
472
- return invoke_agent_prompt(prompt, args.llm)
473
- return {
1077
+ personality = load_personality()
1078
+ name = public_name(personality=personality, invoked_as=getattr(args, "prog_name", "agent"))
1079
+ if is_identity_question(prompt):
1080
+ result = {
1081
+ "kind": "agent",
1082
+ "status": "ok",
1083
+ "ok": True,
1084
+ "requires_llm": False,
1085
+ "prompt_received": True,
1086
+ "prompt_length": len(prompt),
1087
+ "identity": {"name": name, "source": "local"},
1088
+ "response": local_identity_response(prompt, name=name),
1089
+ }
1090
+ return finalize_agent_session(result, session, prompt, backend=args.llm)
1091
+ natural_result = dispatch_natural_operational_prompt(prompt)
1092
+ if natural_result:
1093
+ return finalize_agent_session(natural_result, session, prompt, backend=args.llm)
1094
+ route = route_prompt(prompt)
1095
+ if route:
1096
+ result = invoke_deterministic_route(prompt, route)
1097
+ return finalize_agent_session(result, session, prompt, backend=args.llm)
1098
+ contextual_prompt = build_contextual_prompt(str(session["id"]), prompt)
1099
+ model_plan = build_model_plan(prompt)
1100
+ result = invoke_agent_prompt(
1101
+ contextual_prompt,
1102
+ args.llm,
1103
+ public_name=name,
1104
+ allow_fallback=not args.no_llm_fallback,
1105
+ )
1106
+ review_gate = build_review_gate(prompt, model_plan=model_plan)
1107
+ if result.get("ok"):
1108
+ review_gate = mark_reviewed(review_gate, reviewer=str(result.get("llm_backend") or "coordinator"))
1109
+ result["model_plan"] = model_plan
1110
+ result["review_gate"] = review_gate
1111
+ result["prompt_length"] = len(prompt)
1112
+ result["session_context_applied"] = contextual_prompt != prompt
1113
+ if result.get("response"):
1114
+ result["response"] = enforce_identity_response(str(result["response"]), prompt, name=name)
1115
+ result["identity"] = {"name": name, "source": "local"}
1116
+ return finalize_agent_session(result, session, prompt, backend=result.get("llm_backend") or args.llm)
1117
+
1118
+
1119
+ def dispatch_natural_operational_prompt(prompt: str) -> dict[str, Any] | None:
1120
+ normalized = " ".join(prompt.lower().split())
1121
+ control_result = dispatch_natural_control_prompt(normalized)
1122
+ if control_result:
1123
+ control_result["prompt_received"] = True
1124
+ control_result["prompt_length"] = len(prompt)
1125
+ return control_result
1126
+ if "agenda" in normalized:
1127
+ if "amanha" in normalized or "amanhã" in normalized:
1128
+ payload = calendar_tomorrow()
1129
+ else:
1130
+ payload = calendar_today()
1131
+ payload = dict(payload)
1132
+ payload["kind"] = "agent"
1133
+ payload["mode"] = "calendar-route"
1134
+ payload["requires_llm"] = False
1135
+ payload["prompt_received"] = True
1136
+ payload["prompt_length"] = len(prompt)
1137
+ payload["response"] = calendar_summary(payload)
1138
+ if payload.get("status") == "needs-input":
1139
+ payload["ok"] = False
1140
+ else:
1141
+ payload["ok"] = True
1142
+ return payload
1143
+ if has_pr_intent(normalized):
1144
+ if any(marker in normalized for marker in ("diariamente", "todo dia", "diaria", "diária", "recorrente")):
1145
+ payload = pr_create_automation()
1146
+ return {
1147
+ "kind": "agent",
1148
+ "status": payload.get("status"),
1149
+ "ok": True,
1150
+ "mode": "pr-automation-route",
1151
+ "requires_llm": False,
1152
+ "prompt_received": True,
1153
+ "prompt_length": len(prompt),
1154
+ "response": "Automacao diaria de revisao de PRs criada em modo report-only.",
1155
+ "result": payload,
1156
+ }
1157
+ payload = pr_list_review_requests()
1158
+ return {
1159
+ "kind": "agent",
1160
+ "status": payload.get("status"),
1161
+ "ok": payload.get("status") == "ok",
1162
+ "mode": "pr-route",
1163
+ "requires_llm": False,
1164
+ "prompt_received": True,
1165
+ "prompt_length": len(prompt),
1166
+ "response": summarize_pr_list(payload),
1167
+ "result": payload,
1168
+ "exit_code": payload.get("exit_code", 0 if payload.get("status") == "ok" else 2),
1169
+ }
1170
+ return None
1171
+
1172
+
1173
+ def dispatch_natural_control_prompt(normalized_prompt: str) -> dict[str, Any] | None:
1174
+ if "decis" in normalized_prompt and any(marker in normalized_prompt for marker in ("mostre", "liste", "ver", "mostrar", "listar")):
1175
+ return {
1176
+ "kind": "agent",
1177
+ "status": "ok",
1178
+ "ok": True,
1179
+ "mode": "control-center-route",
1180
+ "requires_llm": False,
1181
+ "response": "Estas sao as decisoes locais registradas.",
1182
+ "result": list_decisions(),
1183
+ }
1184
+ control_targets = {
1185
+ "ollama": ("tools", "ollama"),
1186
+ "azure devops": ("tools", "azure-devops"),
1187
+ "azure": ("tools", "azure-devops"),
1188
+ "github": ("integrations", "github"),
1189
+ "figma": ("tools", "figma"),
1190
+ }
1191
+ action: str | None = None
1192
+ if any(marker in normalized_prompt for marker in ("desative", "desabilite", "disable", "bloqueie")):
1193
+ action = "disable"
1194
+ if any(marker in normalized_prompt for marker in ("reative", "ative", "habilite", "enable")):
1195
+ action = "enable"
1196
+ if not action:
1197
+ return None
1198
+ for marker, (category, item_id) in control_targets.items():
1199
+ if marker in normalized_prompt:
1200
+ state = "enabled" if action == "enable" else "disabled_by_user"
1201
+ result = set_decision(category, item_id, state, reason=f"natural prompt: {action}")
1202
+ return {
1203
+ "kind": "agent",
1204
+ "status": "ok",
1205
+ "ok": True,
1206
+ "mode": "control-center-route",
1207
+ "requires_llm": False,
1208
+ "response": f"{item_id} foi {'ativado' if state == 'enabled' else 'desativado'} para {category}.",
1209
+ "result": {
1210
+ "category": category,
1211
+ "id": item_id,
1212
+ "state": state,
1213
+ "decision": result.get("item"),
1214
+ },
1215
+ }
1216
+ if any(marker in normalized_prompt for marker in ("ferramentas", "tools")) and any(
1217
+ marker in normalized_prompt for marker in ("mostre", "liste", "ver", "mostrar", "listar")
1218
+ ):
1219
+ return {
1220
+ "kind": "agent",
1221
+ "status": "ok",
1222
+ "ok": True,
1223
+ "mode": "control-center-route",
1224
+ "requires_llm": False,
1225
+ "response": "Estas sao as ferramentas com decisoes locais registradas.",
1226
+ "result": list_decisions("tools"),
1227
+ }
1228
+ return None
1229
+
1230
+
1231
+ def build_agent_dry_run_plan(prompt: str, args: argparse.Namespace) -> dict[str, Any]:
1232
+ normalized = " ".join(prompt.lower().split())
1233
+ route = route_prompt(prompt)
1234
+ model_plan = build_model_plan(prompt, route=route)
1235
+ review_gate = build_review_gate(prompt, route=route, model_plan=model_plan)
1236
+ plan: dict[str, Any] = {
474
1237
  "kind": "agent",
475
- "status": "blocked",
476
- "ok": False,
477
- "requires_llm": True,
478
- "llm_backend": args.llm,
1238
+ "status": "planned",
1239
+ "ok": True,
1240
+ "dry_run": True,
1241
+ "requires_llm": False,
479
1242
  "prompt_received": True,
480
1243
  "prompt_length": len(prompt),
481
- "message": "agent requires a configured LLM backend for natural-language tasks.",
482
- "next_steps": [
483
- "Use `agent run <agent> <capability>` for deterministic execution without LLM.",
484
- "Configure a backend with `agent llm configure <backend> --set-default`.",
485
- "Inspect available backends with `agent llm list` and their status with `agent llm doctor`.",
486
- ],
487
- "exit_code": 2,
1244
+ "mode": "dry-run",
1245
+ "intent": "llm" if not route else route.get("intent"),
1246
+ "route": route,
1247
+ "llm_backend": getattr(args, "llm", None),
1248
+ "external_writes": False,
1249
+ "providers": {"used": [], "missing": [], "skipped": []},
1250
+ "commands": [],
1251
+ "permissions": [],
1252
+ "model_plan": model_plan,
1253
+ "review_gate": review_gate,
1254
+ "response": "Dry-run: nenhuma chamada LLM ou escrita externa foi executada.",
488
1255
  }
1256
+ if "agenda" in normalized:
1257
+ plan.update(
1258
+ {
1259
+ "intent": "calendar",
1260
+ "mode": "calendar-dry-run",
1261
+ "providers": {"used": ["calendar"], "missing": [], "skipped": []},
1262
+ "data_reads": ["configured calendar provider, if present"],
1263
+ "response": "Dry-run: o calendario seria consultado localmente se configurado.",
1264
+ }
1265
+ )
1266
+ return plan
1267
+ if has_pr_intent(normalized):
1268
+ recurring = any(marker in normalized for marker in ("diariamente", "todo dia", "diaria", "diária", "recorrente"))
1269
+ plan.update(
1270
+ {
1271
+ "intent": "github-pr-review",
1272
+ "mode": "pr-dry-run",
1273
+ "providers": {"used": ["github"], "missing": [], "skipped": []},
1274
+ "commands": planned_pr_commands("list-review-requests"),
1275
+ "external_writes": False,
1276
+ "permissions": [{"agent": "github-pr-reviewer", "provider": "github", "required_level": "read-only"}],
1277
+ "response": (
1278
+ "Dry-run: a automacao diaria de PR seria planejada em modo report-only."
1279
+ if recurring
1280
+ else "Dry-run: PRs aguardando revisao seriam listadas via gh em modo report-only."
1281
+ ),
1282
+ }
1283
+ )
1284
+ return plan
1285
+ if route:
1286
+ plan["providers"] = {"used": [route.get("provider")], "missing": [], "skipped": []}
1287
+ plan["response"] = "Dry-run: a capability roteada seria executada somente apos validar source/provider."
1288
+ return plan
1289
+ plan["response"] = "Dry-run: o prompt exigiria LLM configurada; nenhuma chamada foi feita."
1290
+ return plan
1291
+
1292
+
1293
+ def has_pr_intent(normalized_prompt: str) -> bool:
1294
+ tokens = {token.strip(".,;:!?()[]{}\"'") for token in normalized_prompt.split()}
1295
+ return bool({"pr", "prs"} & tokens) or "pull request" in normalized_prompt or "pull requests" in normalized_prompt
1296
+
1297
+
1298
+ def summarize_pr_list(payload: dict[str, Any]) -> str:
1299
+ if payload.get("status") != "ok":
1300
+ return str(payload.get("message") or "Nao foi possivel listar PRs.")
1301
+ items = payload.get("items") or []
1302
+ if not items:
1303
+ return "Nenhuma PR aguardando sua revisao foi encontrada."
1304
+ lines = []
1305
+ for item in items:
1306
+ number = item.get("number")
1307
+ title = item.get("title") or "-"
1308
+ url = item.get("url") or ""
1309
+ lines.append(f"- #{number} {title} {url}".strip())
1310
+ return "\n".join(lines)
1311
+
1312
+
1313
+ def finalize_agent_session(
1314
+ result: dict[str, Any],
1315
+ session: dict[str, Any],
1316
+ prompt: str,
1317
+ *,
1318
+ backend: str | None = None,
1319
+ ) -> dict[str, Any]:
1320
+ try:
1321
+ result["session"] = record_exchange(str(session["id"]), prompt=prompt, result=result, backend=backend)
1322
+ except ValueError as exc:
1323
+ raise DevKitError(str(exc)) from exc
1324
+ return result
489
1325
 
490
1326
 
491
1327
  def invoke_deterministic_route(prompt: str, route: dict[str, Any]) -> dict[str, Any]:
@@ -499,21 +1335,25 @@ def invoke_deterministic_route(prompt: str, route: dict[str, Any]) -> dict[str,
499
1335
  raise DevKitError(str(exc)) from exc
500
1336
 
501
1337
  if not source:
1338
+ wizard = missing_source_wizard(prompt, route, root=ROOT)
502
1339
  return {
503
1340
  "kind": "agent",
504
1341
  "status": "needs-input",
505
1342
  "ok": False,
506
1343
  "requires_source": True,
1344
+ "provider": route.get("provider"),
507
1345
  "source_provider": route.get("provider"),
508
1346
  "prompt_received": True,
509
1347
  "prompt_length": len(prompt),
510
1348
  "route": route,
511
1349
  "napkin": napkin_context(ROOT, agent_id=route.get("agent_id")),
512
- "message": "agent identified the task, but no reusable source is configured for this provider.",
1350
+ "setup_wizard": wizard,
1351
+ "next_question": wizard.get("next_question"),
1352
+ "message": wizard.get("message"),
513
1353
  "next_steps": [
514
- "Configure a source with `agent source add azure-sustentacao --provider azure-devops --config project=<project> --env AZURE_DEVOPS_PAT=AZURE_DEVOPS_PAT --default-for card`.",
515
- "Use `--config fixture=<path>` for local fixture-based tests or demos.",
516
- "Rerun the same prompt after configuring the source.",
1354
+ "Responda a pergunta do wizard para autorizar ou negar a configuracao desta fonte.",
1355
+ "Se preferir teste local, configure uma source com fixture sem armazenar segredos.",
1356
+ "O prompt original sera retomado apos a fonte reutilizavel ser configurada.",
517
1357
  ],
518
1358
  "exit_code": 2,
519
1359
  }
@@ -523,6 +1363,10 @@ def invoke_deterministic_route(prompt: str, route: dict[str, Any]) -> dict[str,
523
1363
  result = run_capability(agent, str(route["capability_id"]), capability_args, capture_output=True)
524
1364
  response = result.get("stdout") or result.get("error") or ""
525
1365
  record_usage(prompt, route=route, source_id=str(source["id"]))
1366
+ model_plan = build_model_plan(prompt, route=route)
1367
+ review_gate = build_review_gate(prompt, route=route, model_plan=model_plan)
1368
+ if result.get("ok"):
1369
+ review_gate = mark_reviewed(review_gate, reviewer="deterministic-coordinator")
526
1370
  return {
527
1371
  "kind": "agent",
528
1372
  "status": result.get("status"),
@@ -533,6 +1377,8 @@ def invoke_deterministic_route(prompt: str, route: dict[str, Any]) -> dict[str,
533
1377
  "route": route,
534
1378
  "source": public_source(source),
535
1379
  "napkin": napkin_context(ROOT, agent_id=route.get("agent_id"), source_id=str(source["id"])),
1380
+ "model_plan": model_plan,
1381
+ "review_gate": review_gate,
536
1382
  "response": response,
537
1383
  "result": result,
538
1384
  "exit_code": result.get("exit_code", 0 if result.get("ok") else 1),
@@ -729,9 +1575,9 @@ def run_capability(
729
1575
  next_steps=guardrail["next_steps"],
730
1576
  exit_code=2,
731
1577
  )
732
- readiness = evaluate_provider_requirements(ROOT, data)
1578
+ readiness = evaluate_provider_requirements(ROOT, data, capability_args)
733
1579
  if not readiness["ready"]:
734
- return run_payload(
1580
+ payload = run_payload(
735
1581
  status=readiness["status"],
736
1582
  agent=summarize_agent(agent),
737
1583
  capability=data.get("id", capability_id),
@@ -745,6 +1591,17 @@ def run_capability(
745
1591
  artifacts=readiness["artifacts"],
746
1592
  exit_code=readiness.get("exit_code"),
747
1593
  )
1594
+ wizard = setup_wizard_from_readiness(readiness, agent=summarize_agent(agent), capability_id=str(data.get("id", capability_id)))
1595
+ if wizard:
1596
+ payload["setup_wizard"] = wizard
1597
+ payload["next_question"] = wizard.get("next_question")
1598
+ payload["configuration_agent"] = wizard.get("owner_agent")
1599
+ payload["next_steps"] = [
1600
+ "Responda a pergunta do wizard para autorizar ou negar a configuracao deste provider.",
1601
+ "Informe uma referencia segura de credencial por variavel de ambiente, arquivo ou cadeia nativa quando solicitado.",
1602
+ "Reexecute ou retome a mesma capability depois que a configuracao estiver salva.",
1603
+ ]
1604
+ return payload
748
1605
 
749
1606
  runner_ref = (data.get("entrypoint", {}) or {}).get("runner")
750
1607
  if not runner_ref:
@@ -863,6 +1720,26 @@ def supports_runtime_source(agent_id: str, capability_id: str) -> bool:
863
1720
  return agent_id == "azure-devops-orchestrator" and capability_id == "read-card"
864
1721
 
865
1722
 
1723
+ def setup_wizard_from_readiness(readiness: dict[str, Any], *, agent: dict[str, Any], capability_id: str) -> dict[str, Any] | None:
1724
+ providers = readiness.get("providers") if isinstance(readiness.get("providers"), dict) else {}
1725
+ missing = providers.get("missing") or []
1726
+ if not missing:
1727
+ return None
1728
+ provider_id = str(missing[0])
1729
+ details = providers.get("details") or []
1730
+ detail = next((item for item in details if isinstance(item, dict) and item.get("id") == provider_id), {})
1731
+ try:
1732
+ return provider_wizard_from_requirement(
1733
+ ROOT,
1734
+ provider_id,
1735
+ agent_id=str(agent.get("id") or ""),
1736
+ capability_id=capability_id,
1737
+ reason=str(detail.get("purpose") or "Provider is required but not configured."),
1738
+ )
1739
+ except Exception:
1740
+ return None
1741
+
1742
+
866
1743
  def doctor(project: str | None = None, home: str | None = None, scope: str = "auto") -> dict[str, Any]:
867
1744
  agents = list_agents()
868
1745
  capabilities = list_all_capabilities()
@@ -1050,6 +1927,8 @@ def print_human(result: dict[str, Any]) -> None:
1050
1927
  print_llm_configure(result)
1051
1928
  elif kind == "llm-default":
1052
1929
  print_llm_default(result)
1930
+ elif kind == "llm-preference":
1931
+ print_llm_preference(result)
1053
1932
  elif kind == "providers":
1054
1933
  print_providers(result)
1055
1934
  elif kind == "provider-status":
@@ -1072,8 +1951,54 @@ def print_human(result: dict[str, Any]) -> None:
1072
1951
  print_source_remove(result)
1073
1952
  elif kind == "memory":
1074
1953
  print_memory(result)
1954
+ elif kind == "memory-path":
1955
+ print_memory_path(result)
1075
1956
  elif kind == "memory-reset":
1076
1957
  print_memory_reset(result)
1958
+ elif kind == "personality":
1959
+ print_personality(result)
1960
+ elif kind == "aliases":
1961
+ print_aliases(result)
1962
+ elif kind == "alias":
1963
+ print_alias(result)
1964
+ elif kind == "sessions":
1965
+ print_sessions(result)
1966
+ elif kind == "session":
1967
+ print_session(result)
1968
+ elif kind == "setup":
1969
+ print_setup(result)
1970
+ elif kind == "toolchain":
1971
+ print_toolchain(result)
1972
+ elif kind == "toolchain-doctor":
1973
+ print_toolchain_doctor(result)
1974
+ elif kind == "toolchain-install":
1975
+ print_toolchain_install(result)
1976
+ elif kind == "tasks":
1977
+ print_tasks(result)
1978
+ elif kind == "task":
1979
+ print_task(result)
1980
+ elif kind == "task-history":
1981
+ print_task_history(result)
1982
+ elif kind == "task-run":
1983
+ print_task_run(result)
1984
+ elif kind == "scheduler":
1985
+ print_scheduler(result)
1986
+ elif kind == "calendar":
1987
+ print_calendar(result)
1988
+ elif kind == "calendar-configure":
1989
+ print_calendar_configure(result)
1990
+ elif kind in {"pr", "pr-review", "pr-automation"}:
1991
+ print_pr(result)
1992
+ elif kind == "permissions":
1993
+ print_permissions(result)
1994
+ elif kind in {"audit", "audit-entry", "audit-export"}:
1995
+ print_audit(result)
1996
+ elif kind == "config":
1997
+ print_config(result)
1998
+ elif kind in {"tools", "tool", "integrations", "integration", "skills", "skill", "decisions", "decision", "decisions-reset"}:
1999
+ print_control(result)
2000
+ elif kind in {"ollama-status", "ollama-models", "ollama-pull", "ollama-update"}:
2001
+ print_ollama(result)
1077
2002
  elif kind == "install":
1078
2003
  print_install(result)
1079
2004
  else:
@@ -1201,6 +2126,11 @@ def print_agent_response(result: dict[str, Any]) -> None:
1201
2126
  print(result.get("response", ""))
1202
2127
  return
1203
2128
  print(result.get("message") or result.get("response") or "Agent execution did not complete.")
2129
+ question = result.get("next_question") or ((result.get("setup_wizard") or {}).get("next_question") if isinstance(result.get("setup_wizard"), dict) else None)
2130
+ if isinstance(question, dict) and question.get("text"):
2131
+ print(f"\nPergunta: {question['text']}")
2132
+ if question.get("type") == "confirm":
2133
+ print("[s/N]")
1204
2134
  if result.get("llm_backend"):
1205
2135
  print(f"Requested backend: {result['llm_backend']}")
1206
2136
  if result.get("next_steps"):
@@ -1242,6 +2172,10 @@ def print_source_remove(result: dict[str, Any]) -> None:
1242
2172
 
1243
2173
  def print_memory(result: dict[str, Any]) -> None:
1244
2174
  print(f"Memory home: {result['memory_home']}")
2175
+ if result.get("files"):
2176
+ print("\nFiles:")
2177
+ for item in result["files"]:
2178
+ print(f"- {item['name']}: {item['path']}")
1245
2179
  for bucket in ("prompts", "routes", "sources"):
1246
2180
  print(f"\n{bucket.title()}:")
1247
2181
  items = result["usage"].get(bucket) or []
@@ -1257,6 +2191,218 @@ def print_memory_reset(result: dict[str, Any]) -> None:
1257
2191
  print(f"Config: {result['config_path']}")
1258
2192
 
1259
2193
 
2194
+ def print_memory_path(result: dict[str, Any]) -> None:
2195
+ print(f"Memory home: {result['home']}")
2196
+ if result.get("created"):
2197
+ print("Created:")
2198
+ for path in result["created"]:
2199
+ print(f"- {path}")
2200
+ print("Files:")
2201
+ for item in result["files"]:
2202
+ print(f"- {item['name']}: {item['path']}")
2203
+
2204
+
2205
+ def print_personality(result: dict[str, Any]) -> None:
2206
+ print(f"Personality: {result.get('status', 'ok')}")
2207
+ print(f"Path: {result['path']}")
2208
+ print(f"Agent name: {result.get('agent_name') or '-'}")
2209
+ print(f"User name: {result.get('user_name') or '-'}")
2210
+ print(f"Language: {result.get('language') or '-'}")
2211
+ print(f"Tone: {result.get('tone') or '-'}")
2212
+ print(f"Detail level: {result.get('detail_level') or '-'}")
2213
+ if result.get("message"):
2214
+ print(result["message"])
2215
+ if result.get("questions"):
2216
+ print("Setup questions:")
2217
+ for question in result["questions"]:
2218
+ print(f"- {question}")
2219
+
2220
+
2221
+ def print_aliases(result: dict[str, Any]) -> None:
2222
+ print(f"Aliases config: {result['config_path']}")
2223
+ if not result["items"]:
2224
+ print("No aliases configured.")
2225
+ return
2226
+ for item in result["items"]:
2227
+ print(f"- {item['name']}: {item['path']}")
2228
+
2229
+
2230
+ def print_alias(result: dict[str, Any]) -> None:
2231
+ print(f"Alias {result['status']}: {result['name']}")
2232
+ if result.get("path"):
2233
+ print(f"Path: {result['path']}")
2234
+ if result.get("removed_paths"):
2235
+ print("Removed:")
2236
+ for path in result["removed_paths"]:
2237
+ print(f"- {path}")
2238
+ print(f"Config: {result['config_path']}")
2239
+
2240
+
2241
+ def print_sessions(result: dict[str, Any]) -> None:
2242
+ print(f"Sessions home: {result['home']}")
2243
+ if result.get("active_session_id"):
2244
+ print(f"Active: {result['active_session_id']}")
2245
+ if not result["items"]:
2246
+ print("No sessions found.")
2247
+ return
2248
+ for item in result["items"]:
2249
+ marker = " active" if item.get("active") else ""
2250
+ print(
2251
+ f"- {item['id']}{marker} {item.get('title') or '-'} "
2252
+ f"{item.get('exchange_count', 0)} exchanges ~{item.get('token_estimate', 0)} tokens"
2253
+ )
2254
+ if item.get("project"):
2255
+ print(f" Project: {item['project']}")
2256
+
2257
+
2258
+ def print_session(result: dict[str, Any]) -> None:
2259
+ session = result["session"]
2260
+ print(f"Session {result['status']}: {session['id']}")
2261
+ print(f"Title: {session.get('title') or '-'}")
2262
+ print(f"Path: {session.get('path') or '-'}")
2263
+ print(f"Project: {session.get('project') or '-'}")
2264
+ print(f"Exchanges: {session.get('exchange_count', 0)}")
2265
+ print(f"Token estimate: {session.get('token_estimate', 0)}")
2266
+
2267
+
2268
+ def print_setup(result: dict[str, Any]) -> None:
2269
+ print(f"Setup: {result['status']}")
2270
+ print(f"Dry-run: {result.get('dry_run', False)}")
2271
+ toolchain = result.get("toolchain") or {}
2272
+ print(f"Toolchain: {toolchain.get('status', '-')}")
2273
+ missing = (toolchain.get("required_missing") or []) + (toolchain.get("optional_missing") or [])
2274
+ if missing:
2275
+ print(f"Missing: {', '.join(missing)}")
2276
+ if result.get("next_steps"):
2277
+ print("Next steps:")
2278
+ for step in result["next_steps"]:
2279
+ print(f"- {step}")
2280
+
2281
+
2282
+ def print_toolchain(result: dict[str, Any]) -> None:
2283
+ print(f"Toolchain: {result['status']}")
2284
+ print(f"Platform: {result['platform']}")
2285
+ for item in result["items"]:
2286
+ print(f"- {item['id']} {item['command']} required={item['required']}")
2287
+
2288
+
2289
+ def print_toolchain_doctor(result: dict[str, Any]) -> None:
2290
+ print(f"Toolchain doctor: {result['status']}")
2291
+ print(f"Platform: {result['platform']}")
2292
+ for item in result["items"]:
2293
+ print(f"- {item['id']}: {item['status']}")
2294
+ if item.get("binary"):
2295
+ print(f" {item['binary']}")
2296
+ if item.get("install"):
2297
+ print(f" Install: {item['install']}")
2298
+
2299
+
2300
+ def print_toolchain_install(result: dict[str, Any]) -> None:
2301
+ print(f"Toolchain install: {result['status']}")
2302
+ if result.get("message"):
2303
+ print(result["message"])
2304
+ for plan in result["plans"]:
2305
+ print(f"- {plan['id']}: {plan.get('command') or '-'}")
2306
+
2307
+
2308
+ def print_tasks(result: dict[str, Any]) -> None:
2309
+ print(f"Tasks: {len(result['items'])}")
2310
+ for item in result["items"]:
2311
+ print(f"- {item['id']} {item['status']} {item.get('title') or '-'}")
2312
+
2313
+
2314
+ def print_task(result: dict[str, Any]) -> None:
2315
+ if result.get("message"):
2316
+ print(result["message"])
2317
+ task = result.get("task") or {}
2318
+ if task:
2319
+ print(f"Task {result['status']}: {task.get('id')}")
2320
+ print(f"Title: {task.get('title') or '-'}")
2321
+ print(f"Status: {task.get('status') or '-'}")
2322
+
2323
+
2324
+ def print_task_history(result: dict[str, Any]) -> None:
2325
+ print(result.get("history") or "No history.")
2326
+
2327
+
2328
+ def print_task_run(result: dict[str, Any]) -> None:
2329
+ print(f"Task run: {result['status']}")
2330
+ if result.get("message"):
2331
+ print(result["message"])
2332
+
2333
+
2334
+ def print_scheduler(result: dict[str, Any]) -> None:
2335
+ print(f"Scheduler: {result['status']}")
2336
+ if result.get("message"):
2337
+ print(result["message"])
2338
+ if "due_count" in result:
2339
+ print(f"Due tasks: {result['due_count']}")
2340
+
2341
+
2342
+ def print_calendar(result: dict[str, Any]) -> None:
2343
+ if result.get("status") != "ok":
2344
+ print(result.get("message") or "Calendar is not available.")
2345
+ for step in result.get("next_steps") or []:
2346
+ print(f"- {step}")
2347
+ return
2348
+ print(calendar_summary(result))
2349
+
2350
+
2351
+ def print_calendar_configure(result: dict[str, Any]) -> None:
2352
+ print(f"Calendar configured: {result['provider']}")
2353
+ print(f"Source: {result['source_ref']}")
2354
+ print("Stored secret: no")
2355
+
2356
+
2357
+ def print_pr(result: dict[str, Any]) -> None:
2358
+ if result.get("message"):
2359
+ print(result["message"])
2360
+ if result.get("items") is not None:
2361
+ print(summarize_pr_list(result))
2362
+ elif result.get("summary"):
2363
+ print(result["summary"])
2364
+ elif result.get("task"):
2365
+ task = result["task"]
2366
+ print(f"PR automation {result['status']}: {task.get('id')}")
2367
+
2368
+
2369
+ def print_permissions(result: dict[str, Any]) -> None:
2370
+ print(f"Permissions: {result['status']}")
2371
+ if result.get("default_level"):
2372
+ print(f"Default: {result['default_level']}")
2373
+ if result.get("grant"):
2374
+ grant = result["grant"]
2375
+ print(f"Grant: {grant.get('agent')} / {grant.get('provider')} -> {grant.get('level')}")
2376
+ if result.get("removed") is not None:
2377
+ print(f"Removed: {len(result.get('removed') or [])}")
2378
+ grants = result.get("grants")
2379
+ if grants is not None:
2380
+ if not grants:
2381
+ print("No explicit grants.")
2382
+ for grant in grants:
2383
+ print(f"- {grant.get('agent')} / {grant.get('provider')} -> {grant.get('level')}")
2384
+ if result.get("json_path"):
2385
+ print(f"Policy: {result['json_path']}")
2386
+
2387
+
2388
+ def print_audit(result: dict[str, Any]) -> None:
2389
+ if result["kind"] == "audit":
2390
+ print(f"Audit home: {result['home']}")
2391
+ for item in result.get("items") or []:
2392
+ print(f"- {item.get('id')} {item.get('created_at')} {item.get('command')} {item.get('status')}")
2393
+ return
2394
+ if result["kind"] == "audit-entry":
2395
+ entry = result.get("entry") or {}
2396
+ print(f"Audit: {entry.get('id')}")
2397
+ print(f"Created: {entry.get('created_at')}")
2398
+ print(f"Command: {entry.get('command')}")
2399
+ print(f"Status: {(entry.get('result') or {}).get('status')}")
2400
+ print(f"JSON: {result.get('json_path')}")
2401
+ print(f"Markdown: {result.get('markdown_path')}")
2402
+ return
2403
+ print(result.get("content") or "")
2404
+
2405
+
1260
2406
  def print_llm_backends(result: dict[str, Any]) -> None:
1261
2407
  print(f"LLM config: {result['config_path']}")
1262
2408
  print(f"Default: {result.get('default') or '-'}")
@@ -1294,6 +2440,16 @@ def print_llm_default(result: dict[str, Any]) -> None:
1294
2440
  print(f"Config: {result['config_path']}")
1295
2441
 
1296
2442
 
2443
+ def print_llm_preference(result: dict[str, Any]) -> None:
2444
+ print(f"LLM preference: {result.get('status', 'ok')}")
2445
+ print(f"Primary: {result.get('primary') or '-'}")
2446
+ print(f"Fallback enabled: {result.get('fallback_enabled', True)}")
2447
+ print("Order:")
2448
+ for backend_id in result.get("order") or []:
2449
+ print(f"- {backend_id}")
2450
+ print(f"Config: {result['config_path']}")
2451
+
2452
+
1297
2453
  def print_providers(result: dict[str, Any]) -> None:
1298
2454
  if not result["items"]:
1299
2455
  print("No providers found.")
@@ -1368,6 +2524,58 @@ def print_credential_backends(result: dict[str, Any]) -> None:
1368
2524
  print(f"- {item}")
1369
2525
 
1370
2526
 
2527
+ def print_config(result: dict[str, Any]) -> None:
2528
+ print(f"Config: {result.get('path')}")
2529
+ if result.get("llm"):
2530
+ print(f"Primary LLM: {(result['llm'] or {}).get('primary') or '-'}")
2531
+ if result.get("ollama"):
2532
+ print(f"Ollama: {(result['ollama'] or {}).get('status')}")
2533
+
2534
+
2535
+ def print_control(result: dict[str, Any]) -> None:
2536
+ kind = result.get("kind")
2537
+ if kind == "decisions-reset":
2538
+ print(f"Decisions reset: {result.get('category') or 'all'}")
2539
+ print(f"Path: {result.get('path')}")
2540
+ return
2541
+ if "items" in result:
2542
+ print(f"{kind}:")
2543
+ for item in result.get("items") or []:
2544
+ print(f"- {item.get('category')}:{item.get('id')} {item.get('state')}")
2545
+ if not result.get("items"):
2546
+ print("- none")
2547
+ return
2548
+ item = result.get("item") or result
2549
+ print(f"{item.get('category') or result.get('category')}:{item.get('id') or result.get('id')} {item.get('state') or result.get('state')}")
2550
+
2551
+
2552
+ def print_ollama(result: dict[str, Any]) -> None:
2553
+ kind = result.get("kind")
2554
+ if kind == "ollama-status":
2555
+ print(f"Ollama: {result.get('status')}")
2556
+ print(f"Binary: {result.get('binary') or '-'}")
2557
+ print(f"Version: {result.get('version') or '-'}")
2558
+ daemon = result.get("daemon") or {}
2559
+ print(f"Daemon: {daemon.get('status') or '-'}")
2560
+ print(f"Models: {result.get('model_count', 0)}")
2561
+ if result.get("install_plan"):
2562
+ print(f"Install: {(result['install_plan'] or {}).get('command')}")
2563
+ return
2564
+ if kind == "ollama-models":
2565
+ print(f"Ollama models: {result.get('status')}")
2566
+ for item in result.get("items") or []:
2567
+ print(f"- {item.get('name')} {item.get('size') or '-'}")
2568
+ if not result.get("items"):
2569
+ print("- none")
2570
+ return
2571
+ print(f"{kind}: {result.get('status')}")
2572
+ if result.get("command"):
2573
+ command = result["command"]
2574
+ print("Command: " + (" ".join(command) if isinstance(command, list) else str(command)))
2575
+ if result.get("message"):
2576
+ print(result["message"])
2577
+
2578
+
1371
2579
  def print_install(result: dict[str, Any]) -> None:
1372
2580
  print(f"AI DevKit install: {result['status']}")
1373
2581
  print(f"Scope: {result['scope']}")