agent-devkit 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +66 -13
  2. package/bin/agent.mjs +133 -7
  3. package/package.json +1 -1
  4. package/runtime/README.md +205 -13
  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 +47 -8
  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 +2 -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 +146 -8
  121. package/runtime/cli/aikit/contribution.py +132 -2
  122. package/runtime/cli/aikit/doctor_runtime.py +85 -0
  123. package/runtime/cli/aikit/embedded_mini_brain.py +351 -0
  124. package/runtime/cli/aikit/eval.py +356 -10
  125. package/runtime/cli/aikit/human_output.py +310 -4
  126. package/runtime/cli/aikit/interactive_wizard.py +146 -0
  127. package/runtime/cli/aikit/knowledge_base.py +1067 -0
  128. package/runtime/cli/aikit/llm.py +40 -6
  129. package/runtime/cli/aikit/local_artifacts.py +444 -0
  130. package/runtime/cli/aikit/local_llm.py +176 -0
  131. package/runtime/cli/aikit/local_llm_operator.py +15 -5
  132. package/runtime/cli/aikit/main.py +15 -0
  133. package/runtime/cli/aikit/mcp_manifest.py +798 -0
  134. package/runtime/cli/aikit/mcp_tools.py +643 -5
  135. package/runtime/cli/aikit/memory.py +405 -0
  136. package/runtime/cli/aikit/mini_brain.py +56 -25
  137. package/runtime/cli/aikit/model_router.py +42 -9
  138. package/runtime/cli/aikit/natural_prompt_runtime.py +194 -2
  139. package/runtime/cli/aikit/ollama.py +64 -15
  140. package/runtime/cli/aikit/onboarding.py +551 -0
  141. package/runtime/cli/aikit/output.py +67 -0
  142. package/runtime/cli/aikit/prompt_injection.py +12 -1
  143. package/runtime/cli/aikit/review_gate.py +14 -2
  144. package/runtime/cli/aikit/roadmap_cli.py +1 -1
  145. package/runtime/cli/aikit/secrets.py +3 -2
  146. package/runtime/cli/aikit/setup_wizard_payload.py +3 -0
  147. package/runtime/cli/aikit/shared_memory.py +415 -0
  148. package/runtime/cli/aikit/specialist_readiness.py +152 -0
  149. package/runtime/cli/aikit/tasks.py +104 -1
  150. package/runtime/cli/aikit/team.py +380 -0
  151. package/runtime/cli/aikit/toolchain.py +7 -2
  152. package/runtime/cli/aikit/workflows.py +115 -14
  153. package/runtime/models/qwen2.5-0.5b-instruct/manifest.json +30 -0
  154. package/runtime/providers/knowledge-github.yaml +40 -0
  155. package/runtime/providers/knowledge-google-drive.yaml +32 -0
  156. package/runtime/providers/knowledge-local.yaml +26 -0
  157. package/runtime/providers/knowledge-notion.yaml +32 -0
  158. package/runtime/providers/knowledge-obsidian.yaml +24 -0
  159. package/runtime/providers/knowledge-onedrive.yaml +36 -0
  160. package/runtime/providers/knowledge-s3.yaml +45 -0
  161. package/runtime/providers/knowledge-sharepoint.yaml +39 -0
  162. package/runtime/providers/knowledge-supabase.yaml +43 -0
  163. package/runtime/providers/knowledge-vector.yaml +39 -0
  164. package/runtime/requirements.txt +6 -0
  165. package/runtime/scripts/docker-cli-qa.sh +453 -0
  166. package/runtime/scripts/release-catalog-snapshot.json +55 -4
  167. package/runtime/scripts/release-gate.py +54 -13
  168. package/runtime/tooling/toolchain.yaml +92 -0
  169. package/runtime/vendor/skills/napkin/napkin.md +21 -7
  170. package/runtime/workflows/azure-card-analysis/README.md +3 -0
  171. package/runtime/workflows/azure-card-analysis/workflow.yaml +30 -0
  172. package/runtime/workflows/daily-pr-review/README.md +3 -0
  173. package/runtime/workflows/daily-pr-review/workflow.yaml +31 -0
  174. package/runtime/workflows/incident-analysis/README.md +3 -0
  175. package/runtime/workflows/incident-analysis/workflow.yaml +33 -0
  176. package/runtime/workflows/release-prep/README.md +3 -0
  177. package/runtime/workflows/release-prep/workflow.yaml +30 -0
@@ -14,6 +14,14 @@ from pathlib import Path
14
14
  from typing import Any
15
15
 
16
16
  from cli.aikit.app_home import app_home, config_path as app_config_path, ensure_app_home
17
+ from cli.aikit.embedded_mini_brain import (
18
+ EMBEDDED_BACKEND_ID,
19
+ EMBEDDED_MODEL_ID,
20
+ EmbeddedMiniBrainError,
21
+ embedded_backend_config,
22
+ embedded_backend_doctor,
23
+ invoke_embedded_mini_brain,
24
+ )
17
25
  from cli.aikit.identity import host_cli_prompt, identity_system_prompt
18
26
 
19
27
 
@@ -33,6 +41,14 @@ class LlmBackend:
33
41
 
34
42
 
35
43
  BACKENDS: dict[str, LlmBackend] = {
44
+ EMBEDDED_BACKEND_ID: LlmBackend(
45
+ id=EMBEDDED_BACKEND_ID,
46
+ display_name="Embedded mini-brain",
47
+ kind="embedded-local",
48
+ auth="none",
49
+ default_model=EMBEDDED_MODEL_ID,
50
+ notes="Uses the Agent DevKit embedded mini-brain for setup, onboarding and low-risk conversation.",
51
+ ),
36
52
  "openai": LlmBackend(
37
53
  id="openai",
38
54
  display_name="OpenAI API",
@@ -77,7 +93,7 @@ BACKENDS: dict[str, LlmBackend] = {
77
93
  base_url_env="OLLAMA_BASE_URL",
78
94
  model_env="OLLAMA_MODEL",
79
95
  default_base_url="http://localhost:11434/v1",
80
- default_model="qwen2.5-coder",
96
+ default_model="qwen3:0.6b",
81
97
  notes="Uses a local Ollama server through an OpenAI-compatible endpoint.",
82
98
  ),
83
99
  "codex-cli": LlmBackend(
@@ -100,7 +116,7 @@ BACKENDS: dict[str, LlmBackend] = {
100
116
 
101
117
  ENV_VAR_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
102
118
  DEFAULT_AGENT_TIMEOUT_SECONDS = 120
103
- DEFAULT_FALLBACK_ORDER = ("claude-code", "codex-cli", "openai", "anthropic", "openrouter", "ollama")
119
+ DEFAULT_FALLBACK_ORDER = ("claude-code", "codex-cli", "openai", "anthropic", "openrouter", "ollama", EMBEDDED_BACKEND_ID)
104
120
 
105
121
 
106
122
  def config_home() -> Path:
@@ -323,6 +339,8 @@ def normalize_backend_order(order: str | list[str] | tuple[str, ...]) -> list[st
323
339
 
324
340
 
325
341
  def default_backend_config(backend: LlmBackend) -> dict[str, Any]:
342
+ if backend.id == EMBEDDED_BACKEND_ID:
343
+ return embedded_backend_config()
326
344
  entry: dict[str, Any] = {"kind": backend.kind, "auth": backend.auth}
327
345
  if backend.auth == "api-key-env":
328
346
  entry["api_key_ref"] = f"env:{backend.api_key_env}"
@@ -346,7 +364,8 @@ def doctor_backends(backend_id: str | None = None) -> dict[str, Any]:
346
364
 
347
365
  checks = [doctor_backend(BACKENDS[item], config) for item in ids]
348
366
  status = "ok"
349
- if any(item["status"] == "missing" for item in checks):
367
+ missing_statuses = {"missing", "not-installed", "dependency-missing", "invalid-model"}
368
+ if any(item["status"] in missing_statuses for item in checks):
350
369
  status = "partial" if not backend_id else "missing"
351
370
  if any(item["status"] == "error" for item in checks):
352
371
  status = "error"
@@ -361,6 +380,8 @@ def doctor_backends(backend_id: str | None = None) -> dict[str, Any]:
361
380
 
362
381
 
363
382
  def doctor_backend(backend: LlmBackend, config: dict[str, Any]) -> dict[str, Any]:
383
+ if backend.id == EMBEDDED_BACKEND_ID:
384
+ return embedded_backend_doctor()
364
385
  configured = config.get("llm", {}).get("backends", {}).get(backend.id, {})
365
386
  if not isinstance(configured, dict):
366
387
  configured = {}
@@ -387,18 +408,26 @@ def doctor_backend(backend: LlmBackend, config: dict[str, Any]) -> dict[str, Any
387
408
  }
388
409
 
389
410
  if backend.auth == "none":
390
- base_url = configured.get("base_url") or env_value(backend.base_url_env) or backend.default_base_url
411
+ env_base_url = env_value(backend.base_url_env)
412
+ base_url = configured.get("base_url") or env_base_url or backend.default_base_url
391
413
  model = configured.get("model") or env_value(backend.model_env) or backend.default_model
414
+ binary = shutil.which(backend.id) if backend.id == "ollama" else None
415
+ local_available = bool(configured or env_base_url or binary)
392
416
  return {
393
417
  "id": backend.id,
394
418
  "display_name": backend.display_name,
395
419
  "kind": backend.kind,
396
- "status": "ok" if base_url else "missing",
420
+ "status": "ok" if local_available else "missing",
397
421
  "configured": bool(configured),
398
422
  "base_url": base_url,
399
423
  "model": model,
424
+ "binary": binary,
400
425
  "health": "unchecked",
401
- "message": "Local backend configured; daemon health is not probed by default.",
426
+ "message": (
427
+ "Local backend configured; daemon health is not probed by default."
428
+ if local_available
429
+ else "Local backend is not configured and no local binary was found in PATH."
430
+ ),
402
431
  }
403
432
 
404
433
  api_key_env = configured.get("api_key_env") or backend.api_key_env
@@ -608,6 +637,11 @@ class LlmPolicyError(LlmInvocationError):
608
637
  def invoke_resolved_backend(backend: dict[str, Any], prompt: str, *, public_name: str = "Agent DevKit") -> str:
609
638
  kind = backend.get("kind")
610
639
  backend_id = backend.get("id")
640
+ if kind == "embedded-local" and backend_id == EMBEDDED_BACKEND_ID:
641
+ try:
642
+ return invoke_embedded_mini_brain(prompt, public_name=public_name)
643
+ except EmbeddedMiniBrainError as exc:
644
+ raise LlmInvocationError(str(exc)) from exc
611
645
  if kind == "openai-compatible":
612
646
  return invoke_openai_compatible(backend, prompt, public_name=public_name)
613
647
  if kind == "anthropic":
@@ -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"