agent-devkit 0.2.0 → 0.3.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 (172) hide show
  1. package/README.md +48 -6
  2. package/bin/agent.mjs +133 -7
  3. package/package.json +1 -1
  4. package/runtime/README.md +187 -5
  5. package/runtime/agent +31 -5
  6. package/runtime/agents/README.md +18 -0
  7. package/runtime/agents/contribution-reviewer/AGENTS.md +8 -0
  8. package/runtime/agents/contribution-reviewer/README.md +8 -0
  9. package/runtime/agents/contribution-reviewer/agent.yaml +40 -0
  10. package/runtime/agents/contribution-reviewer/capabilities/plan-contribution-pr/capability.yaml +27 -0
  11. package/runtime/agents/contribution-reviewer/capabilities/plan-contribution-pr/decision-rules.md +5 -0
  12. package/runtime/agents/contribution-reviewer/capabilities/plan-contribution-pr/workflow.md +6 -0
  13. package/runtime/agents/contribution-reviewer/capabilities/review-contribution/capability.yaml +25 -0
  14. package/runtime/agents/contribution-reviewer/capabilities/review-contribution/decision-rules.md +5 -0
  15. package/runtime/agents/contribution-reviewer/capabilities/review-contribution/workflow.md +5 -0
  16. package/runtime/agents/contribution-reviewer/capabilities/validate-local-contribution/capability.yaml +26 -0
  17. package/runtime/agents/contribution-reviewer/capabilities/validate-local-contribution/decision-rules.md +5 -0
  18. package/runtime/agents/contribution-reviewer/capabilities/validate-local-contribution/workflow.md +6 -0
  19. package/runtime/agents/contribution-reviewer/infra/README.md +6 -0
  20. package/runtime/agents/contribution-reviewer/knowledge/context.md +8 -0
  21. package/runtime/agents/contribution-reviewer/knowledge/system.md +8 -0
  22. package/runtime/agents/contribution-reviewer/templates/README.md +3 -0
  23. package/runtime/agents/knowledge-author/AGENTS.md +7 -0
  24. package/runtime/agents/knowledge-author/README.md +7 -0
  25. package/runtime/agents/knowledge-author/agent.yaml +37 -0
  26. package/runtime/agents/knowledge-author/capabilities/create-knowledge-snapshot/capability.yaml +30 -0
  27. package/runtime/agents/knowledge-author/capabilities/create-knowledge-snapshot/decision-rules.md +6 -0
  28. package/runtime/agents/knowledge-author/capabilities/create-knowledge-snapshot/workflow.md +7 -0
  29. package/runtime/agents/knowledge-author/infra/.gitkeep +1 -0
  30. package/runtime/agents/knowledge-author/knowledge/context.md +4 -0
  31. package/runtime/agents/knowledge-author/knowledge/system.md +4 -0
  32. package/runtime/agents/knowledge-author/templates/.gitkeep +1 -0
  33. package/runtime/agents/knowledge-curator/AGENTS.md +7 -0
  34. package/runtime/agents/knowledge-curator/README.md +6 -0
  35. package/runtime/agents/knowledge-curator/agent.yaml +37 -0
  36. package/runtime/agents/knowledge-curator/capabilities/curate-knowledge-base/capability.yaml +29 -0
  37. package/runtime/agents/knowledge-curator/capabilities/curate-knowledge-base/decision-rules.md +6 -0
  38. package/runtime/agents/knowledge-curator/capabilities/curate-knowledge-base/workflow.md +7 -0
  39. package/runtime/agents/knowledge-curator/infra/.gitkeep +1 -0
  40. package/runtime/agents/knowledge-curator/knowledge/context.md +4 -0
  41. package/runtime/agents/knowledge-curator/knowledge/system.md +4 -0
  42. package/runtime/agents/knowledge-curator/templates/.gitkeep +1 -0
  43. package/runtime/agents/knowledge-infra-builder/AGENTS.md +8 -0
  44. package/runtime/agents/knowledge-infra-builder/README.md +8 -0
  45. package/runtime/agents/knowledge-infra-builder/agent.yaml +38 -0
  46. package/runtime/agents/knowledge-infra-builder/capabilities/create-knowledge-base/capability.yaml +30 -0
  47. package/runtime/agents/knowledge-infra-builder/capabilities/create-knowledge-base/decision-rules.md +6 -0
  48. package/runtime/agents/knowledge-infra-builder/capabilities/create-knowledge-base/workflow.md +7 -0
  49. package/runtime/agents/knowledge-infra-builder/infra/.gitkeep +1 -0
  50. package/runtime/agents/knowledge-infra-builder/knowledge/context.md +4 -0
  51. package/runtime/agents/knowledge-infra-builder/knowledge/system.md +4 -0
  52. package/runtime/agents/knowledge-infra-builder/templates/.gitkeep +1 -0
  53. package/runtime/agents/knowledge-owner/AGENTS.md +7 -0
  54. package/runtime/agents/knowledge-owner/README.md +6 -0
  55. package/runtime/agents/knowledge-owner/agent.yaml +37 -0
  56. package/runtime/agents/knowledge-owner/capabilities/publish-knowledge-snapshot/capability.yaml +28 -0
  57. package/runtime/agents/knowledge-owner/capabilities/publish-knowledge-snapshot/decision-rules.md +6 -0
  58. package/runtime/agents/knowledge-owner/capabilities/publish-knowledge-snapshot/workflow.md +7 -0
  59. package/runtime/agents/knowledge-owner/infra/.gitkeep +1 -0
  60. package/runtime/agents/knowledge-owner/knowledge/context.md +4 -0
  61. package/runtime/agents/knowledge-owner/knowledge/system.md +4 -0
  62. package/runtime/agents/knowledge-owner/templates/.gitkeep +1 -0
  63. package/runtime/agents/knowledge-reviewer/AGENTS.md +7 -0
  64. package/runtime/agents/knowledge-reviewer/README.md +7 -0
  65. package/runtime/agents/knowledge-reviewer/agent.yaml +36 -0
  66. package/runtime/agents/knowledge-reviewer/capabilities/review-knowledge-snapshot/capability.yaml +26 -0
  67. package/runtime/agents/knowledge-reviewer/capabilities/review-knowledge-snapshot/decision-rules.md +6 -0
  68. package/runtime/agents/knowledge-reviewer/capabilities/review-knowledge-snapshot/workflow.md +7 -0
  69. package/runtime/agents/knowledge-reviewer/infra/.gitkeep +1 -0
  70. package/runtime/agents/knowledge-reviewer/knowledge/context.md +4 -0
  71. package/runtime/agents/knowledge-reviewer/knowledge/system.md +4 -0
  72. package/runtime/agents/knowledge-reviewer/templates/.gitkeep +1 -0
  73. package/runtime/agents/local-memory-manager/AGENTS.md +5 -0
  74. package/runtime/agents/local-memory-manager/README.md +7 -0
  75. package/runtime/agents/local-memory-manager/agent.yaml +38 -0
  76. package/runtime/agents/local-memory-manager/capabilities/curate-local-memory/capability.yaml +19 -0
  77. package/runtime/agents/local-memory-manager/capabilities/curate-local-memory/decision-rules.md +5 -0
  78. package/runtime/agents/local-memory-manager/capabilities/curate-local-memory/workflow.md +6 -0
  79. package/runtime/agents/local-memory-manager/capabilities/inspect-local-memory/capability.yaml +19 -0
  80. package/runtime/agents/local-memory-manager/capabilities/inspect-local-memory/decision-rules.md +5 -0
  81. package/runtime/agents/local-memory-manager/capabilities/inspect-local-memory/workflow.md +5 -0
  82. package/runtime/agents/local-memory-manager/infra/.gitkeep +1 -0
  83. package/runtime/agents/local-memory-manager/knowledge/context.md +4 -0
  84. package/runtime/agents/local-memory-manager/knowledge/system.md +4 -0
  85. package/runtime/agents/local-memory-manager/templates/.gitkeep +1 -0
  86. package/runtime/agents/memory-sync-manager/AGENTS.md +7 -0
  87. package/runtime/agents/memory-sync-manager/README.md +7 -0
  88. package/runtime/agents/memory-sync-manager/agent.yaml +37 -0
  89. package/runtime/agents/memory-sync-manager/capabilities/plan-memory-backup/capability.yaml +29 -0
  90. package/runtime/agents/memory-sync-manager/capabilities/plan-memory-backup/decision-rules.md +6 -0
  91. package/runtime/agents/memory-sync-manager/capabilities/plan-memory-backup/workflow.md +7 -0
  92. package/runtime/agents/memory-sync-manager/infra/.gitkeep +1 -0
  93. package/runtime/agents/memory-sync-manager/knowledge/context.md +4 -0
  94. package/runtime/agents/memory-sync-manager/knowledge/system.md +4 -0
  95. package/runtime/agents/memory-sync-manager/templates/.gitkeep +1 -0
  96. package/runtime/agents/shared-memory-curator/AGENTS.md +5 -0
  97. package/runtime/agents/shared-memory-curator/README.md +6 -0
  98. package/runtime/agents/shared-memory-curator/agent.yaml +38 -0
  99. package/runtime/agents/shared-memory-curator/capabilities/create-shared-memory/capability.yaml +19 -0
  100. package/runtime/agents/shared-memory-curator/capabilities/create-shared-memory/decision-rules.md +5 -0
  101. package/runtime/agents/shared-memory-curator/capabilities/create-shared-memory/workflow.md +5 -0
  102. package/runtime/agents/shared-memory-curator/capabilities/publish-shared-submission/capability.yaml +19 -0
  103. package/runtime/agents/shared-memory-curator/capabilities/publish-shared-submission/decision-rules.md +5 -0
  104. package/runtime/agents/shared-memory-curator/capabilities/publish-shared-submission/workflow.md +5 -0
  105. package/runtime/agents/shared-memory-curator/capabilities/review-shared-submission/capability.yaml +19 -0
  106. package/runtime/agents/shared-memory-curator/capabilities/review-shared-submission/decision-rules.md +5 -0
  107. package/runtime/agents/shared-memory-curator/capabilities/review-shared-submission/workflow.md +5 -0
  108. package/runtime/agents/shared-memory-curator/infra/.gitkeep +1 -0
  109. package/runtime/agents/shared-memory-curator/knowledge/context.md +5 -0
  110. package/runtime/agents/shared-memory-curator/knowledge/system.md +4 -0
  111. package/runtime/agents/shared-memory-curator/templates/.gitkeep +1 -0
  112. package/runtime/cli/README.md +35 -4
  113. package/runtime/cli/aikit/__init__.py +1 -1
  114. package/runtime/cli/aikit/agent_registry.py +4 -2
  115. package/runtime/cli/aikit/agentic_commands.py +158 -0
  116. package/runtime/cli/aikit/app_home.py +1 -0
  117. package/runtime/cli/aikit/audit.py +16 -6
  118. package/runtime/cli/aikit/catalog.py +278 -8
  119. package/runtime/cli/aikit/cli_dispatch.py +489 -13
  120. package/runtime/cli/aikit/cli_parser.py +145 -7
  121. package/runtime/cli/aikit/contribution.py +132 -2
  122. package/runtime/cli/aikit/doctor_runtime.py +85 -0
  123. package/runtime/cli/aikit/eval.py +356 -10
  124. package/runtime/cli/aikit/human_output.py +310 -4
  125. package/runtime/cli/aikit/interactive_wizard.py +148 -0
  126. package/runtime/cli/aikit/knowledge_base.py +1067 -0
  127. package/runtime/cli/aikit/llm.py +12 -4
  128. package/runtime/cli/aikit/local_artifacts.py +444 -0
  129. package/runtime/cli/aikit/local_llm.py +161 -0
  130. package/runtime/cli/aikit/main.py +15 -0
  131. package/runtime/cli/aikit/mcp_manifest.py +798 -0
  132. package/runtime/cli/aikit/mcp_tools.py +643 -5
  133. package/runtime/cli/aikit/memory.py +405 -0
  134. package/runtime/cli/aikit/mini_brain.py +20 -1
  135. package/runtime/cli/aikit/natural_prompt_runtime.py +125 -1
  136. package/runtime/cli/aikit/ollama.py +64 -15
  137. package/runtime/cli/aikit/onboarding.py +551 -0
  138. package/runtime/cli/aikit/output.py +67 -0
  139. package/runtime/cli/aikit/prompt_injection.py +12 -1
  140. package/runtime/cli/aikit/roadmap_cli.py +1 -1
  141. package/runtime/cli/aikit/secrets.py +3 -2
  142. package/runtime/cli/aikit/setup_wizard_payload.py +3 -0
  143. package/runtime/cli/aikit/shared_memory.py +415 -0
  144. package/runtime/cli/aikit/specialist_readiness.py +152 -0
  145. package/runtime/cli/aikit/tasks.py +104 -1
  146. package/runtime/cli/aikit/team.py +380 -0
  147. package/runtime/cli/aikit/toolchain.py +7 -2
  148. package/runtime/cli/aikit/workflows.py +115 -14
  149. package/runtime/providers/knowledge-github.yaml +40 -0
  150. package/runtime/providers/knowledge-google-drive.yaml +32 -0
  151. package/runtime/providers/knowledge-local.yaml +26 -0
  152. package/runtime/providers/knowledge-notion.yaml +32 -0
  153. package/runtime/providers/knowledge-obsidian.yaml +24 -0
  154. package/runtime/providers/knowledge-onedrive.yaml +36 -0
  155. package/runtime/providers/knowledge-s3.yaml +45 -0
  156. package/runtime/providers/knowledge-sharepoint.yaml +39 -0
  157. package/runtime/providers/knowledge-supabase.yaml +43 -0
  158. package/runtime/providers/knowledge-vector.yaml +39 -0
  159. package/runtime/requirements.txt +6 -0
  160. package/runtime/scripts/docker-cli-qa.sh +453 -0
  161. package/runtime/scripts/release-catalog-snapshot.json +55 -4
  162. package/runtime/scripts/release-gate.py +54 -13
  163. package/runtime/tooling/toolchain.yaml +92 -0
  164. package/runtime/vendor/skills/napkin/napkin.md +21 -7
  165. package/runtime/workflows/azure-card-analysis/README.md +3 -0
  166. package/runtime/workflows/azure-card-analysis/workflow.yaml +30 -0
  167. package/runtime/workflows/daily-pr-review/README.md +3 -0
  168. package/runtime/workflows/daily-pr-review/workflow.yaml +31 -0
  169. package/runtime/workflows/incident-analysis/README.md +3 -0
  170. package/runtime/workflows/incident-analysis/workflow.yaml +33 -0
  171. package/runtime/workflows/release-prep/README.md +3 -0
  172. package/runtime/workflows/release-prep/workflow.yaml +30 -0
@@ -77,7 +77,7 @@ BACKENDS: dict[str, LlmBackend] = {
77
77
  base_url_env="OLLAMA_BASE_URL",
78
78
  model_env="OLLAMA_MODEL",
79
79
  default_base_url="http://localhost:11434/v1",
80
- default_model="qwen2.5-coder",
80
+ default_model="qwen3:0.6b",
81
81
  notes="Uses a local Ollama server through an OpenAI-compatible endpoint.",
82
82
  ),
83
83
  "codex-cli": LlmBackend(
@@ -387,18 +387,26 @@ def doctor_backend(backend: LlmBackend, config: dict[str, Any]) -> dict[str, Any
387
387
  }
388
388
 
389
389
  if backend.auth == "none":
390
- base_url = configured.get("base_url") or env_value(backend.base_url_env) or backend.default_base_url
390
+ env_base_url = env_value(backend.base_url_env)
391
+ base_url = configured.get("base_url") or env_base_url or backend.default_base_url
391
392
  model = configured.get("model") or env_value(backend.model_env) or backend.default_model
393
+ binary = shutil.which(backend.id) if backend.id == "ollama" else None
394
+ local_available = bool(configured or env_base_url or binary)
392
395
  return {
393
396
  "id": backend.id,
394
397
  "display_name": backend.display_name,
395
398
  "kind": backend.kind,
396
- "status": "ok" if base_url else "missing",
399
+ "status": "ok" if local_available else "missing",
397
400
  "configured": bool(configured),
398
401
  "base_url": base_url,
399
402
  "model": model,
403
+ "binary": binary,
400
404
  "health": "unchecked",
401
- "message": "Local backend configured; daemon health is not probed by default.",
405
+ "message": (
406
+ "Local backend configured; daemon health is not probed by default."
407
+ if local_available
408
+ else "Local backend is not configured and no local binary was found in PATH."
409
+ ),
402
410
  }
403
411
 
404
412
  api_key_env = configured.get("api_key_env") or backend.api_key_env
@@ -0,0 +1,444 @@
1
+ """Local user-created skills, scripts and agent scaffolds."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from cli.aikit.app_home import app_home, ensure_app_home
14
+ from cli.aikit.errors import DevKitError
15
+
16
+
17
+ LOCAL_ARTIFACT_SCHEMA_VERSION = "agent-devkit.local-artifacts/v1"
18
+ ID_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
19
+
20
+
21
+ def skill_create(skill_id: str | None, *, description: str | None = None, force: bool = False) -> dict[str, Any]:
22
+ item_id = require_id(skill_id, "skill id")
23
+ path = local_home("skills") / item_id
24
+ if path.exists() and not force:
25
+ raise DevKitError(f"local skill already exists: {item_id}")
26
+ path.mkdir(parents=True, exist_ok=True)
27
+ skill_path = path / "SKILL.md"
28
+ if force or not skill_path.exists():
29
+ skill_path.write_text(render_skill(item_id, description), encoding="utf-8")
30
+ return local_artifact_payload("local-skill", "created", item_id, path, write_policy="local_config_write")
31
+
32
+
33
+ def skill_list() -> dict[str, Any]:
34
+ return local_artifact_list("local-skills", "skills", marker="SKILL.md")
35
+
36
+
37
+ def skill_show(skill_id: str | None) -> dict[str, Any]:
38
+ return local_artifact_show("local-skill", "skills", require_id(skill_id, "skill id"), marker="SKILL.md")
39
+
40
+
41
+ def skill_update(skill_id: str | None, *, description: str | None = None) -> dict[str, Any]:
42
+ item_id = require_id(skill_id, "skill id")
43
+ path = local_home("skills") / item_id
44
+ if not path.exists():
45
+ raise DevKitError(f"local skill not found: {item_id}")
46
+ (path / "SKILL.md").write_text(render_skill(item_id, description), encoding="utf-8")
47
+ return local_artifact_payload("local-skill", "updated", item_id, path, write_policy="local_config_write")
48
+
49
+
50
+ def skill_delete(skill_id: str | None, *, yes: bool = False) -> dict[str, Any]:
51
+ item_id = require_id(skill_id, "skill id")
52
+ path = local_home("skills") / item_id
53
+ if not yes:
54
+ payload = local_artifact_payload("local-skill", "needs-confirmation", item_id, path, write_policy="local_config_write")
55
+ payload["ok"] = False
56
+ payload["exit_code"] = 2
57
+ return payload
58
+ shutil.rmtree(path, ignore_errors=True)
59
+ return local_artifact_payload("local-skill", "deleted", item_id, path, write_policy="local_config_write")
60
+
61
+
62
+ def script_create(script_id: str | None, *, command: str | None = None, force: bool = False) -> dict[str, Any]:
63
+ item_id = require_id(script_id, "script id")
64
+ path = local_home("scripts") / f"{item_id}.sh"
65
+ if path.exists() and not force:
66
+ raise DevKitError(f"local script already exists: {item_id}")
67
+ path.parent.mkdir(parents=True, exist_ok=True)
68
+ path.write_text(render_script(command), encoding="utf-8")
69
+ path.chmod(0o755)
70
+ return local_artifact_payload("local-script", "created", item_id, path, write_policy="local_write")
71
+
72
+
73
+ def script_list() -> dict[str, Any]:
74
+ home = local_home("scripts")
75
+ items = [
76
+ local_artifact_item(path.stem, path, "local-script", enabled=True)
77
+ for path in sorted(home.glob("*.sh"))
78
+ ]
79
+ return {"kind": "local-scripts", "schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION, "status": "ok", "home": str(home), "items": items}
80
+
81
+
82
+ def script_run(script_id: str | None, *, dry_run: bool = False, yes: bool = False) -> dict[str, Any]:
83
+ item_id = require_id(script_id, "script id")
84
+ path = local_home("scripts") / f"{item_id}.sh"
85
+ if not path.exists():
86
+ raise DevKitError(f"local script not found: {item_id}")
87
+ if dry_run or not yes:
88
+ return {
89
+ "kind": "local-script-run",
90
+ "schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION,
91
+ "status": "planned" if dry_run else "needs-confirmation",
92
+ "ok": bool(dry_run),
93
+ "id": item_id,
94
+ "path": str(path),
95
+ "command": [str(path)],
96
+ "write_policy": "local_write",
97
+ "message": "Use --yes to run the local script." if not dry_run and not yes else "Dry-run only.",
98
+ **({} if dry_run else {"exit_code": 2}),
99
+ }
100
+ process = subprocess.run([str(path)], check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=300)
101
+ return {
102
+ "kind": "local-script-run",
103
+ "schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION,
104
+ "status": "ok" if process.returncode == 0 else "failed",
105
+ "ok": process.returncode == 0,
106
+ "id": item_id,
107
+ "path": str(path),
108
+ "exit_code": process.returncode,
109
+ "stdout": process.stdout[-4000:],
110
+ "stderr": process.stderr[-4000:],
111
+ "write_policy": "local_write",
112
+ }
113
+
114
+
115
+ def local_agent_create(agent_id: str | None, *, description: str | None = None, force: bool = False) -> dict[str, Any]:
116
+ item_id = require_id(agent_id, "agent id")
117
+ path = local_home("agents") / item_id
118
+ if path.exists() and not force:
119
+ raise DevKitError(f"local agent already exists: {item_id}")
120
+ (path / "capabilities").mkdir(parents=True, exist_ok=True)
121
+ (path / "knowledge").mkdir(parents=True, exist_ok=True)
122
+ (path / "templates").mkdir(parents=True, exist_ok=True)
123
+ (path / "infra").mkdir(parents=True, exist_ok=True)
124
+ (path / "agent.yaml").write_text(render_agent_yaml(item_id, description), encoding="utf-8")
125
+ (path / "README.md").write_text(f"# {item_id}\n\n{description or 'Local Agent DevKit agent.'}\n", encoding="utf-8")
126
+ return local_artifact_payload("local-agent", "created", item_id, path, write_policy="local_config_write")
127
+
128
+
129
+ def local_agent_validate(agent_id: str | None) -> dict[str, Any]:
130
+ item_id = require_id(agent_id, "agent id")
131
+ path = local_home("agents") / item_id
132
+ checks = [
133
+ {"id": "path-exists", "status": "passed" if path.exists() else "failed"},
134
+ {"id": "agent-yaml", "status": "passed" if (path / "agent.yaml").exists() else "failed"},
135
+ {"id": "capabilities-dir", "status": "passed" if (path / "capabilities").is_dir() else "failed"},
136
+ {"id": "knowledge-dir", "status": "passed" if (path / "knowledge").is_dir() else "failed"},
137
+ ]
138
+ return {
139
+ "kind": "local-agent-validation",
140
+ "schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION,
141
+ "status": "passed" if all(check["status"] == "passed" for check in checks) else "failed",
142
+ "id": item_id,
143
+ "path": str(path),
144
+ "checks": checks,
145
+ }
146
+
147
+
148
+ def local_agent_show(agent_id: str | None) -> dict[str, Any]:
149
+ item_id = require_id(agent_id, "agent id")
150
+ path = local_home("agents") / item_id
151
+ if not (path / "agent.yaml").exists():
152
+ raise DevKitError(f"local agent not found: {item_id}")
153
+ payload = local_artifact_payload("local-agent", "ok", item_id, path, write_policy="read_only")
154
+ payload["manifest"] = (path / "agent.yaml").read_text(encoding="utf-8")
155
+ readme = path / "README.md"
156
+ if readme.exists():
157
+ payload["readme"] = readme.read_text(encoding="utf-8")
158
+ return payload
159
+
160
+
161
+ def local_agent_list() -> dict[str, Any]:
162
+ return local_artifact_list("local-agents", "agents", marker="agent.yaml")
163
+
164
+
165
+ def local_automation_create(
166
+ automation_id: str | None,
167
+ *,
168
+ title: str | None = None,
169
+ prompt: str | None = None,
170
+ command: str | None = None,
171
+ every: str | None = None,
172
+ cron: str | None = None,
173
+ force: bool = False,
174
+ ) -> dict[str, Any]:
175
+ item_id = generated_id(automation_id, title, prefix="automation")
176
+ path = local_home("automations") / item_id
177
+ manifest = path / "automation.json"
178
+ if manifest.exists() and not force:
179
+ raise DevKitError(f"local automation already exists: {item_id}")
180
+ path.mkdir(parents=True, exist_ok=True)
181
+ payload = automation_manifest(item_id, title=title, prompt=prompt, command=command, every=every, cron=cron, enabled=True)
182
+ write_json(manifest, payload)
183
+ if command:
184
+ script_path = path / "run.sh"
185
+ script_path.write_text(render_script(command), encoding="utf-8")
186
+ script_path.chmod(0o755)
187
+ return local_automation_payload("created", item_id, path, payload)
188
+
189
+
190
+ def local_automation_list() -> dict[str, Any]:
191
+ home = local_home("automations")
192
+ items = []
193
+ for manifest in sorted(home.glob("*/automation.json")):
194
+ payload = read_json(manifest)
195
+ item_id = str(payload.get("id") or manifest.parent.name)
196
+ items.append(
197
+ {
198
+ "id": item_id,
199
+ "kind": "local-automation",
200
+ "path": str(manifest.parent),
201
+ "enabled": payload.get("enabled") is True,
202
+ "title": payload.get("title"),
203
+ "schedule": payload.get("schedule") or {},
204
+ "write_policy": payload.get("write_policy") or "local_write",
205
+ }
206
+ )
207
+ return {"kind": "local-automations", "schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION, "status": "ok", "home": str(home), "items": items}
208
+
209
+
210
+ def local_automation_show(automation_id: str | None) -> dict[str, Any]:
211
+ item_id = require_id(automation_id, "automation id")
212
+ path = local_home("automations") / item_id
213
+ payload = read_required_automation(path, item_id)
214
+ return local_automation_payload("ok", item_id, path, payload)
215
+
216
+
217
+ def local_automation_update(
218
+ automation_id: str | None,
219
+ *,
220
+ title: str | None = None,
221
+ prompt: str | None = None,
222
+ command: str | None = None,
223
+ every: str | None = None,
224
+ cron: str | None = None,
225
+ ) -> dict[str, Any]:
226
+ item_id = require_id(automation_id, "automation id")
227
+ path = local_home("automations") / item_id
228
+ current = read_required_automation(path, item_id)
229
+ current_schedule = current.get("schedule") if isinstance(current.get("schedule"), dict) else {"type": "manual"}
230
+ updated = automation_manifest(
231
+ item_id,
232
+ title=title if title is not None else str(current.get("title") or ""),
233
+ prompt=prompt if prompt is not None else current.get("prompt"),
234
+ command=command if command is not None else current.get("command"),
235
+ every=every,
236
+ cron=cron,
237
+ enabled=current.get("enabled") is not False,
238
+ created_at=str(current.get("created_at") or now_iso()),
239
+ )
240
+ if every is None and cron is None:
241
+ updated["schedule"] = current_schedule
242
+ write_json(path / "automation.json", updated)
243
+ return local_automation_payload("updated", item_id, path, updated)
244
+
245
+
246
+ def local_automation_enable(automation_id: str | None, enabled: bool) -> dict[str, Any]:
247
+ item_id = require_id(automation_id, "automation id")
248
+ path = local_home("automations") / item_id
249
+ payload = read_required_automation(path, item_id)
250
+ payload["enabled"] = enabled
251
+ payload["updated_at"] = now_iso()
252
+ write_json(path / "automation.json", payload)
253
+ return local_automation_payload("enabled" if enabled else "disabled", item_id, path, payload)
254
+
255
+
256
+ def local_automation_remove(automation_id: str | None, *, yes: bool = False) -> dict[str, Any]:
257
+ item_id = require_id(automation_id, "automation id")
258
+ path = local_home("automations") / item_id
259
+ if not yes:
260
+ payload = local_artifact_payload("local-automation", "needs-confirmation", item_id, path, write_policy="local_config_write")
261
+ payload["ok"] = False
262
+ payload["exit_code"] = 2
263
+ return payload
264
+ shutil.rmtree(path, ignore_errors=True)
265
+ return local_artifact_payload("local-automation", "removed", item_id, path, write_policy="local_config_write")
266
+
267
+
268
+ def local_automation_validate(automation_id: str | None) -> dict[str, Any]:
269
+ item_id = require_id(automation_id, "automation id")
270
+ path = local_home("automations") / item_id
271
+ payload = read_json(path / "automation.json") if (path / "automation.json").exists() else {}
272
+ checks = [
273
+ {"id": "path-exists", "status": "passed" if path.exists() else "failed"},
274
+ {"id": "manifest-exists", "status": "passed" if (path / "automation.json").exists() else "failed"},
275
+ {"id": "has-action", "status": "passed" if payload.get("prompt") or payload.get("command") else "failed"},
276
+ {"id": "no-stored-secret", "status": "passed" if not contains_secret_like_text(json.dumps(payload, ensure_ascii=False)) else "failed"},
277
+ ]
278
+ return {
279
+ "kind": "local-automation-validation",
280
+ "schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION,
281
+ "status": "passed" if all(check["status"] == "passed" for check in checks) else "failed",
282
+ "id": item_id,
283
+ "path": str(path),
284
+ "checks": checks,
285
+ }
286
+
287
+
288
+ def local_home(*parts: str) -> Path:
289
+ ensure_app_home()
290
+ path = app_home() / "local" / Path(*parts)
291
+ path.mkdir(parents=True, exist_ok=True)
292
+ return path
293
+
294
+
295
+ def local_artifact_list(kind: str, folder: str, *, marker: str) -> dict[str, Any]:
296
+ home = local_home(folder)
297
+ items = [
298
+ local_artifact_item(path.parent.name, path.parent, kind.removesuffix("s"), enabled=True)
299
+ for path in sorted(home.glob(f"*/{marker}"))
300
+ ]
301
+ return {"kind": kind, "schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION, "status": "ok", "home": str(home), "items": items}
302
+
303
+
304
+ def local_artifact_show(kind: str, folder: str, item_id: str, *, marker: str) -> dict[str, Any]:
305
+ path = local_home(folder) / item_id
306
+ marker_path = path / marker
307
+ if not marker_path.exists():
308
+ raise DevKitError(f"{kind} not found: {item_id}")
309
+ payload = local_artifact_payload(kind, "ok", item_id, path, write_policy="read_only")
310
+ payload["content"] = marker_path.read_text(encoding="utf-8")
311
+ return payload
312
+
313
+
314
+ def local_artifact_payload(kind: str, status: str, item_id: str, path: Path, *, write_policy: str) -> dict[str, Any]:
315
+ return {
316
+ "kind": kind,
317
+ "schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION,
318
+ "status": status,
319
+ "id": item_id,
320
+ "path": str(path),
321
+ "write_policy": write_policy,
322
+ "stored_secret": False,
323
+ }
324
+
325
+
326
+ def local_artifact_item(item_id: str, path: Path, kind: str, *, enabled: bool) -> dict[str, Any]:
327
+ return {"id": item_id, "kind": kind, "path": str(path), "enabled": enabled}
328
+
329
+
330
+ def require_id(value: str | None, label: str) -> str:
331
+ item_id = (value or "").strip()
332
+ if not ID_PATTERN.fullmatch(item_id):
333
+ raise DevKitError(f"{label} must use lowercase letters, numbers, dots, dashes or underscores")
334
+ return item_id
335
+
336
+
337
+ def generated_id(value: str | None, title: str | None, *, prefix: str) -> str:
338
+ if value:
339
+ return require_id(value, f"{prefix} id")
340
+ raw = title or f"{prefix}-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}"
341
+ slug = re.sub(r"[^a-z0-9._-]+", "-", raw.strip().lower()).strip("-._")
342
+ return require_id(slug or prefix, f"{prefix} id")
343
+
344
+
345
+ def automation_manifest(
346
+ item_id: str,
347
+ *,
348
+ title: str | None,
349
+ prompt: Any,
350
+ command: Any,
351
+ every: str | None,
352
+ cron: str | None,
353
+ enabled: bool,
354
+ created_at: str | None = None,
355
+ ) -> dict[str, Any]:
356
+ schedule: dict[str, Any] = {"type": "manual"}
357
+ if every:
358
+ schedule = {"type": "interval", "every": every}
359
+ if cron:
360
+ schedule = {"type": "cron", "cron": cron}
361
+ now = now_iso()
362
+ return {
363
+ "schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION,
364
+ "id": item_id,
365
+ "kind": "local-automation",
366
+ "title": title or item_id,
367
+ "prompt": prompt,
368
+ "command": command,
369
+ "schedule": schedule,
370
+ "enabled": enabled,
371
+ "write_policy": "local_write" if command else "local_config_write",
372
+ "external_writes": False,
373
+ "stored_secret": False,
374
+ "created_at": created_at or now,
375
+ "updated_at": now,
376
+ }
377
+
378
+
379
+ def local_automation_payload(status: str, item_id: str, path: Path, manifest: dict[str, Any]) -> dict[str, Any]:
380
+ payload = local_artifact_payload("local-automation", status, item_id, path, write_policy=str(manifest.get("write_policy") or "local_config_write"))
381
+ payload["automation"] = manifest
382
+ return payload
383
+
384
+
385
+ def read_required_automation(path: Path, item_id: str) -> dict[str, Any]:
386
+ manifest = path / "automation.json"
387
+ if not manifest.exists():
388
+ raise DevKitError(f"local automation not found: {item_id}")
389
+ return read_json(manifest)
390
+
391
+
392
+ def read_json(path: Path) -> dict[str, Any]:
393
+ try:
394
+ payload = json.loads(path.read_text(encoding="utf-8"))
395
+ except (OSError, json.JSONDecodeError):
396
+ return {}
397
+ return payload if isinstance(payload, dict) else {}
398
+
399
+
400
+ def write_json(path: Path, payload: dict[str, Any]) -> None:
401
+ path.parent.mkdir(parents=True, exist_ok=True)
402
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
403
+
404
+
405
+ def contains_secret_like_text(text: str) -> bool:
406
+ return bool(re.search(r"(?i)(api[_-]?key|token|secret|password|senha|pat)\s*[:=]", text))
407
+
408
+
409
+ def now_iso() -> str:
410
+ return datetime.now(timezone.utc).isoformat()
411
+
412
+
413
+ def render_skill(skill_id: str, description: str | None) -> str:
414
+ return f"""---
415
+ name: {skill_id}
416
+ description: {description or 'Local Agent DevKit skill.'}
417
+ ---
418
+
419
+ # {skill_id}
420
+
421
+ {description or 'Local skill created by Agent DevKit.'}
422
+ """
423
+
424
+
425
+ def render_script(command: str | None) -> str:
426
+ body = command or "echo 'local script placeholder'"
427
+ return f"#!/usr/bin/env sh\nset -eu\n{body}\n"
428
+
429
+
430
+ def render_agent_yaml(agent_id: str, description: str | None) -> str:
431
+ return json.dumps(
432
+ {
433
+ "id": agent_id,
434
+ "kind": "agent",
435
+ "name": agent_id,
436
+ "version": "0.1.0",
437
+ "status": "draft",
438
+ "purpose": description or "Local Agent DevKit agent.",
439
+ "default_context": ["knowledge/context.md"],
440
+ "capabilities": [],
441
+ },
442
+ ensure_ascii=False,
443
+ indent=2,
444
+ ) + "\n"
@@ -0,0 +1,161 @@
1
+ """Canonical local-LLM CLI surface over Ollama and mini-brain policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+ from typing import Any
8
+
9
+ from cli.aikit.mini_brain import DEFAULT_OLLAMA_MODEL, mini_brain_contract
10
+ from cli.aikit.model_router import build_model_plan
11
+ from cli.aikit.ollama import ollama_models, ollama_pull, ollama_status
12
+
13
+
14
+ LOCAL_LLM_SCHEMA_VERSION = "agent-devkit.local-llm/v1"
15
+ LOCAL_WORKERS = (
16
+ ("local-classifier", "intent classification and routing hints"),
17
+ ("local-summarizer", "short summaries and extraction"),
18
+ ("local-coder", "mechanical code drafts with coordinator review"),
19
+ ("local-log-analyzer", "log clustering and evidence extraction"),
20
+ ("local-sql-helper", "SQL draft assistance with review required"),
21
+ )
22
+
23
+
24
+ def local_llm_list() -> dict[str, Any]:
25
+ status = ollama_status()
26
+ contract = mini_brain_contract(ollama_payload=status)
27
+ return {
28
+ "kind": "local-llm",
29
+ "schema_version": LOCAL_LLM_SCHEMA_VERSION,
30
+ "status": "ok",
31
+ "provider": "ollama",
32
+ "mini_brain": contract,
33
+ "workers": [{"id": worker_id, "purpose": purpose} for worker_id, purpose in LOCAL_WORKERS],
34
+ "models": {
35
+ "status": status.get("status"),
36
+ "count": status.get("model_count", 0),
37
+ },
38
+ }
39
+
40
+
41
+ def local_llm_doctor() -> dict[str, Any]:
42
+ status = ollama_status()
43
+ contract = mini_brain_contract(ollama_payload=status)
44
+ model_plan = build_model_plan("resuma estes logs operacionais")
45
+ ok = status.get("status") == "ok" and contract.get("enabled") is True
46
+ return {
47
+ "kind": "local-llm-doctor",
48
+ "schema_version": LOCAL_LLM_SCHEMA_VERSION,
49
+ "status": "ok" if ok else "partial",
50
+ "provider": "ollama",
51
+ "ollama": status,
52
+ "mini_brain": contract,
53
+ "model_plan": {
54
+ "strategy": model_plan.get("strategy"),
55
+ "local_llm_recommended": model_plan.get("local_llm_recommended"),
56
+ "local_llm_selected": model_plan.get("local_llm_selected"),
57
+ "local_llm_role": model_plan.get("local_llm_role"),
58
+ "forbidden_actions": model_plan.get("forbidden_actions") or [],
59
+ },
60
+ "next_steps": local_llm_next_steps(status=status, mini_brain=contract),
61
+ }
62
+
63
+
64
+ def local_llm_models() -> dict[str, Any]:
65
+ payload = ollama_models()
66
+ payload["kind"] = "local-llm-models"
67
+ payload["schema_version"] = LOCAL_LLM_SCHEMA_VERSION
68
+ payload["provider"] = "ollama"
69
+ return payload
70
+
71
+
72
+ def local_llm_install(model: str | None, *, dry_run: bool = False, yes: bool = False) -> dict[str, Any]:
73
+ model = model or DEFAULT_OLLAMA_MODEL
74
+ payload = ollama_pull(model, dry_run=dry_run, yes=yes)
75
+ payload["kind"] = "local-llm-install"
76
+ payload["schema_version"] = LOCAL_LLM_SCHEMA_VERSION
77
+ payload["provider"] = "ollama"
78
+ return payload
79
+
80
+
81
+ def local_llm_remove(model: str | None, *, dry_run: bool = False, yes: bool = False) -> dict[str, Any]:
82
+ model = model or DEFAULT_OLLAMA_MODEL
83
+ command = ["ollama", "rm", model]
84
+ binary = shutil.which("ollama")
85
+ if dry_run or not yes:
86
+ needs_confirmation = not dry_run and not yes
87
+ return {
88
+ "kind": "local-llm-remove",
89
+ "schema_version": LOCAL_LLM_SCHEMA_VERSION,
90
+ "status": "planned" if dry_run else "needs-confirmation",
91
+ "ok": bool(dry_run),
92
+ "exit_code": 2 if needs_confirmation else 0,
93
+ "provider": "ollama",
94
+ "model": model,
95
+ "binary": binary,
96
+ "command": command,
97
+ "dry_run": dry_run,
98
+ "yes": yes,
99
+ "message": "Use --yes to remove the local model." if not dry_run and not yes else "Dry-run only.",
100
+ }
101
+ if not binary:
102
+ return {
103
+ "kind": "local-llm-remove",
104
+ "schema_version": LOCAL_LLM_SCHEMA_VERSION,
105
+ "status": "missing",
106
+ "ok": False,
107
+ "provider": "ollama",
108
+ "model": model,
109
+ "command": command,
110
+ "message": "Ollama binary not found in PATH.",
111
+ "exit_code": 2,
112
+ }
113
+ process = subprocess.run(command, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
114
+ return {
115
+ "kind": "local-llm-remove",
116
+ "schema_version": LOCAL_LLM_SCHEMA_VERSION,
117
+ "status": "ok" if process.returncode == 0 else "failed",
118
+ "ok": process.returncode == 0,
119
+ "provider": "ollama",
120
+ "model": model,
121
+ "binary": binary,
122
+ "command": command,
123
+ "exit_code": process.returncode,
124
+ "stdout": process.stdout[-4000:],
125
+ "stderr": process.stderr[-4000:],
126
+ }
127
+
128
+
129
+ def local_llm_benchmark(model: str | None = None) -> dict[str, Any]:
130
+ model = model or DEFAULT_OLLAMA_MODEL
131
+ status = ollama_status()
132
+ installed = {item.get("name") for item in status.get("models") or [] if isinstance(item, dict)}
133
+ available = status.get("status") == "ok" and model in installed
134
+ return {
135
+ "kind": "local-llm-benchmark",
136
+ "schema_version": LOCAL_LLM_SCHEMA_VERSION,
137
+ "status": "ready" if available else "planned",
138
+ "ok": available,
139
+ "provider": "ollama",
140
+ "model": model,
141
+ "checks": [
142
+ {"id": "ollama-binary", "status": "passed" if status.get("status") == "ok" else "failed"},
143
+ {"id": "model-installed", "status": "passed" if model in installed else "planned"},
144
+ {"id": "policy", "status": "passed", "message": "Benchmark is read-only and does not approve final work."},
145
+ ],
146
+ "next_steps": [] if available else [f"agent local-llm install {model} --yes"],
147
+ }
148
+
149
+
150
+ def local_llm_next_steps(*, status: dict[str, Any], mini_brain: dict[str, Any]) -> list[str]:
151
+ steps: list[str] = []
152
+ if status.get("status") != "ok":
153
+ install_plan = status.get("install_plan") if isinstance(status.get("install_plan"), dict) else {}
154
+ command = install_plan.get("command")
155
+ if command:
156
+ steps.append(f"Install Ollama: {command}")
157
+ if not mini_brain.get("enabled"):
158
+ steps.append("agent setup mini-brain --yes")
159
+ if status.get("model_count", 0) < 1:
160
+ steps.append(f"agent local-llm install {DEFAULT_OLLAMA_MODEL} --yes")
161
+ return steps
@@ -4,7 +4,9 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import json
7
+ import os
7
8
  import sys
9
+ import traceback
8
10
 
9
11
  from cli.aikit.cli_dispatch import dispatch, format_audit_warning, is_audit_warning, maybe_record_cli_audit
10
12
  from cli.aikit.cli_parser import DETERMINISTIC_COMMANDS, LLM_COMMANDS, build_parser
@@ -26,6 +28,19 @@ def main(argv: list[str] | None = None, *, prog: str | None = None) -> int:
26
28
  print(format_audit_warning(audit_result), file=sys.stderr)
27
29
  print(f"error: {exc}", file=sys.stderr)
28
30
  return 1
31
+ except Exception as exc: # noqa: BLE001 - CLI boundary must not leak tracebacks by default.
32
+ if os.environ.get("AI_DEVKIT_DEBUG") == "1":
33
+ traceback.print_exc(file=sys.stderr)
34
+ else:
35
+ print(
36
+ f"error: internal error: {type(exc).__name__}. "
37
+ "Run with AI_DEVKIT_DEBUG=1 for traceback.",
38
+ file=sys.stderr,
39
+ )
40
+ audit_result = maybe_record_cli_audit(args, result=None, error=f"{type(exc).__name__}: {exc}")
41
+ if is_audit_warning(audit_result):
42
+ print(format_audit_warning(audit_result), file=sys.stderr)
43
+ return 1
29
44
 
30
45
  if result is None:
31
46
  return 0