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
@@ -100,7 +100,7 @@ def create_task(
100
100
  "created_at": created_at,
101
101
  "updated_at": created_at,
102
102
  "schedule": schedule or {"type": "manual"},
103
- "action": action or {"type": "prompt", "prompt": redact_secrets(prompt or "")},
103
+ "action": action or ({"type": "prompt", "prompt": redact_secrets(prompt)} if prompt else {"type": "noop"}),
104
104
  "permissions": permissions or {"mode": "report-only"},
105
105
  "notifications": notifications or [{"type": "terminal"}],
106
106
  "run_count": 0,
@@ -231,6 +231,41 @@ def run_task(
231
231
  payload["events"] = scheduler_events + [scheduler_event_summary(payload)]
232
232
  payload["events_path"] = str(scheduler_events_path())
233
233
  return payload
234
+ action_result = execute_task_action(task, origin=origin)
235
+ action_ok = bool(action_result.get("ok")) or action_result.get("status") == "ok"
236
+ if not action_ok:
237
+ append_history(str(task["id"]), f"Task `{task['id']}` failed: {action_result.get('message') or action_result.get('status')}")
238
+ payload = {
239
+ "kind": "task-run",
240
+ "status": "failed",
241
+ "ok": False,
242
+ "dry_run": False,
243
+ "task": public_task(task),
244
+ "preview": preview,
245
+ "autonomy_contract": preview["autonomy_contract"],
246
+ "result": action_result,
247
+ "message": action_result.get("message") or "Task action failed.",
248
+ "exit_code": int(action_result.get("exit_code") or 1),
249
+ }
250
+ if scheduler_origin:
251
+ finished_at = now_iso()
252
+ attach_scheduler_metadata(
253
+ payload,
254
+ task,
255
+ event="scheduled_task.failed",
256
+ run_id=run_id,
257
+ scheduled_for=scheduled_for,
258
+ started_at=started_at,
259
+ finished_at=finished_at,
260
+ summary="Scheduled task action failed.",
261
+ )
262
+ attach_scheduler_audit(payload, task)
263
+ attach_task_notification(payload, task, event_name(origin, "failed"), origin=origin)
264
+ if scheduler_origin:
265
+ record_scheduler_event(payload)
266
+ payload["events"] = scheduler_events + [scheduler_event_summary(payload)]
267
+ payload["events_path"] = str(scheduler_events_path())
268
+ return payload
234
269
  task["run_count"] = int(task.get("run_count") or 0) + 1
235
270
  task["last_run_at"] = now_iso()
236
271
  task["updated_at"] = task["last_run_at"]
@@ -244,6 +279,8 @@ def run_task(
244
279
  "task": public_task(task),
245
280
  "preview": preview,
246
281
  "autonomy_contract": preview["autonomy_contract"],
282
+ "result": action_result,
283
+ "response": action_result.get("response"),
247
284
  }
248
285
  if scheduler_origin:
249
286
  finished_at = now_iso()
@@ -368,6 +405,72 @@ def event_name(origin: str, outcome: str) -> str:
368
405
  return f"{namespace}.{outcome}"
369
406
 
370
407
 
408
+ def execute_task_action(task: dict[str, Any], *, origin: str) -> dict[str, Any]:
409
+ action = task.get("action") if isinstance(task.get("action"), dict) else {}
410
+ action_type = str(action.get("type") or "prompt")
411
+ if action_type == "noop":
412
+ return {
413
+ "kind": "task-action",
414
+ "status": "ok",
415
+ "ok": True,
416
+ "message": "No-op local task completed.",
417
+ }
418
+ if action_type == "prompt":
419
+ prompt = str(action.get("prompt") or "").strip()
420
+ if not prompt:
421
+ return {
422
+ "kind": "task-action",
423
+ "status": "failed",
424
+ "ok": False,
425
+ "message": "Task prompt is empty.",
426
+ "exit_code": 2,
427
+ }
428
+ from cli.aikit.core.requests import AgentPromptRequest
429
+ from cli.aikit.natural_prompt_runtime import run_agent_prompt_request
430
+
431
+ return run_agent_prompt_request(
432
+ AgentPromptRequest(
433
+ prompt=prompt,
434
+ prog_name="agent",
435
+ project=None,
436
+ new_session=False,
437
+ )
438
+ )
439
+ if action_type == "capability":
440
+ agent_id = str(action.get("agent") or "").strip()
441
+ capability_id = str(action.get("capability") or "").strip()
442
+ if not agent_id or not capability_id:
443
+ return {
444
+ "kind": "task-action",
445
+ "status": "failed",
446
+ "ok": False,
447
+ "message": "Capability task requires agent and capability.",
448
+ "exit_code": 2,
449
+ }
450
+ args = action.get("args") if isinstance(action.get("args"), list) else []
451
+ inputs = action.get("inputs") if isinstance(action.get("inputs"), dict) else {}
452
+ if action.get("external_writes") is True and not args and not inputs:
453
+ return {
454
+ "kind": "task-action",
455
+ "status": "ok",
456
+ "ok": True,
457
+ "agent": agent_id,
458
+ "capability": capability_id,
459
+ "message": "External-write task permission was granted; no capability args were supplied, so no external action was executed.",
460
+ "external_action_executed": False,
461
+ }
462
+ from cli.aikit.capability_runtime import load_agent, run_capability
463
+
464
+ return run_capability(load_agent(agent_id), capability_id, [str(item) for item in args], capture_output=True, origin=origin)
465
+ return {
466
+ "kind": "task-action",
467
+ "status": "failed",
468
+ "ok": False,
469
+ "message": f"Unsupported task action type: {action_type}",
470
+ "exit_code": 2,
471
+ }
472
+
473
+
371
474
  def new_run_id() -> str:
372
475
  return f"run_{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}"
373
476
 
@@ -0,0 +1,380 @@
1
+ """Project-local team profile support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from cli.aikit.errors import DevKitError
11
+
12
+
13
+ TEAM_SCHEMA_VERSION = "agent-devkit.team/v1"
14
+ TEAM_DIR = ".agent-devkit"
15
+ TEAM_FILE = "team.yaml"
16
+ SECRET_KEY_PATTERN = re.compile(r"(token|secret|password|api[_-]?key|pat|credential)", re.IGNORECASE)
17
+ SECRET_VALUE_PATTERN = re.compile(r"(sk-[A-Za-z0-9]|ghp_[A-Za-z0-9]|xox[baprs]-|-----BEGIN [A-Z ]*PRIVATE KEY-----)")
18
+
19
+
20
+ def team_init(project: Path | None = None, *, force: bool = False) -> dict[str, Any]:
21
+ path = team_profile_path(project)
22
+ if path.exists() and not force:
23
+ return team_status(project)
24
+ path.parent.mkdir(parents=True, exist_ok=True)
25
+ payload = default_team_payload()
26
+ write_team_payload(path, payload)
27
+ return {
28
+ "kind": "team",
29
+ "schema_version": TEAM_SCHEMA_VERSION,
30
+ "status": "initialized",
31
+ "path": str(path),
32
+ "profile": payload["active_profile"],
33
+ "secret_free": True,
34
+ }
35
+
36
+
37
+ def team_status(project: Path | None = None) -> dict[str, Any]:
38
+ path = team_profile_path(project)
39
+ if not path.exists():
40
+ return {
41
+ "kind": "team",
42
+ "schema_version": TEAM_SCHEMA_VERSION,
43
+ "status": "not-configured",
44
+ "path": str(path),
45
+ "active_profile": None,
46
+ "profiles": [],
47
+ "next_steps": ["Run `agent team init` to create a project-local team profile."],
48
+ }
49
+ payload = read_team_payload(path)
50
+ findings = secret_findings(payload)
51
+ profiles = payload.get("profiles") if isinstance(payload.get("profiles"), dict) else {}
52
+ active_profile = str(payload.get("active_profile") or "")
53
+ return {
54
+ "kind": "team",
55
+ "schema_version": TEAM_SCHEMA_VERSION,
56
+ "status": "ok" if active_profile in profiles and not findings else "blocked",
57
+ "path": str(path),
58
+ "active_profile": active_profile,
59
+ "profiles": sorted(profiles),
60
+ "secret_free": not findings,
61
+ "findings": findings,
62
+ }
63
+
64
+
65
+ def team_doctor(project: Path | None = None) -> dict[str, Any]:
66
+ status = team_status(project)
67
+ checks = [
68
+ {"id": "team-file-exists", "status": "passed" if status["status"] != "not-configured" else "failed"},
69
+ {"id": "active-profile-exists", "status": "passed" if status.get("active_profile") in set(status.get("profiles") or []) else "failed"},
70
+ {"id": "no-secret-material", "status": "passed" if status.get("secret_free") is not False else "failed"},
71
+ ]
72
+ return {
73
+ "kind": "team-doctor",
74
+ "schema_version": TEAM_SCHEMA_VERSION,
75
+ "status": "ok" if all(check["status"] == "passed" for check in checks) else "blocked",
76
+ "team": status,
77
+ "checks": checks,
78
+ }
79
+
80
+
81
+ def team_onboard(project: Path | None = None) -> dict[str, Any]:
82
+ status = team_status(project)
83
+ if status["status"] == "not-configured":
84
+ return {
85
+ "kind": "team-onboarding",
86
+ "schema_version": TEAM_SCHEMA_VERSION,
87
+ "status": "needs-init",
88
+ "team": status,
89
+ "next_steps": ["Run `agent team init`.", "Review `.agent-devkit/team.yaml` before committing it."],
90
+ }
91
+ return {
92
+ "kind": "team-onboarding",
93
+ "schema_version": TEAM_SCHEMA_VERSION,
94
+ "status": "ok" if status["status"] == "ok" else "blocked",
95
+ "team": status,
96
+ "next_steps": [
97
+ "Configure personal secret refs locally with `agent secret set ... --env ...`.",
98
+ "Run `agent doctor --project .` to validate personal and project setup.",
99
+ "Install desired team workflows locally with `agent workflow install <id> --dry-run` first.",
100
+ ],
101
+ }
102
+
103
+
104
+ def team_profile_list(project: Path | None = None) -> dict[str, Any]:
105
+ payload = require_team_payload(project)
106
+ profiles = payload.get("profiles") if isinstance(payload.get("profiles"), dict) else {}
107
+ return {
108
+ "kind": "team-profiles",
109
+ "schema_version": TEAM_SCHEMA_VERSION,
110
+ "status": "ok",
111
+ "active_profile": payload.get("active_profile"),
112
+ "items": [public_profile(profile_id, profile) for profile_id, profile in sorted(profiles.items())],
113
+ }
114
+
115
+
116
+ def team_profile_show(profile_id: str | None, project: Path | None = None) -> dict[str, Any]:
117
+ payload = require_team_payload(project)
118
+ profile_id = profile_id or str(payload.get("active_profile") or "")
119
+ profile = require_profile(payload, profile_id)
120
+ return {
121
+ "kind": "team-profile",
122
+ "schema_version": TEAM_SCHEMA_VERSION,
123
+ "status": "ok",
124
+ "active": profile_id == payload.get("active_profile"),
125
+ "profile": public_profile(profile_id, profile, detailed=True),
126
+ }
127
+
128
+
129
+ def team_profile_use(profile_id: str | None, project: Path | None = None) -> dict[str, Any]:
130
+ if not profile_id:
131
+ raise DevKitError("team profile use requires a profile id")
132
+ path = team_profile_path(project)
133
+ payload = require_team_payload(project)
134
+ require_profile(payload, profile_id)
135
+ payload["active_profile"] = profile_id
136
+ write_team_payload(path, payload)
137
+ return team_profile_show(profile_id, project)
138
+
139
+
140
+ def team_profile_export(profile_id: str | None, destination: str | None, project: Path | None = None) -> dict[str, Any]:
141
+ payload = require_team_payload(project)
142
+ profile_id = profile_id or str(payload.get("active_profile") or "")
143
+ profile = require_profile(payload, profile_id)
144
+ export_payload = {"schema_version": TEAM_SCHEMA_VERSION, "profile_id": profile_id, "profile": profile}
145
+ findings = secret_findings(export_payload)
146
+ if findings:
147
+ raise DevKitError("team profile export blocked because profile contains secret-like material")
148
+ if not destination:
149
+ return {
150
+ "kind": "team-profile-export",
151
+ "schema_version": TEAM_SCHEMA_VERSION,
152
+ "status": "planned",
153
+ "profile_id": profile_id,
154
+ "payload": export_payload,
155
+ "writes": [],
156
+ }
157
+ target = Path(destination).expanduser().resolve()
158
+ target.parent.mkdir(parents=True, exist_ok=True)
159
+ write_team_payload(target, export_payload)
160
+ return {
161
+ "kind": "team-profile-export",
162
+ "schema_version": TEAM_SCHEMA_VERSION,
163
+ "status": "exported",
164
+ "profile_id": profile_id,
165
+ "path": str(target),
166
+ "writes": [str(target)],
167
+ }
168
+
169
+
170
+ def team_profile_import(source: str | None, project: Path | None = None) -> dict[str, Any]:
171
+ if not source:
172
+ raise DevKitError("team profile import requires a path")
173
+ source_path = Path(source).expanduser().resolve()
174
+ if not source_path.exists():
175
+ raise DevKitError(f"team profile import path not found: {source_path}")
176
+ imported = read_team_payload(source_path)
177
+ findings = secret_findings(imported)
178
+ if findings:
179
+ return {
180
+ "kind": "team-profile-import",
181
+ "schema_version": TEAM_SCHEMA_VERSION,
182
+ "status": "blocked",
183
+ "path": str(source_path),
184
+ "findings": findings,
185
+ }
186
+ profile_id = str(imported.get("profile_id") or imported.get("id") or source_path.stem)
187
+ profile = imported.get("profile") if isinstance(imported.get("profile"), dict) else imported
188
+ path = team_profile_path(project)
189
+ payload = default_team_payload() if not path.exists() else read_team_payload(path)
190
+ profiles = payload.setdefault("profiles", {})
191
+ if not isinstance(profiles, dict):
192
+ raise DevKitError("team profile registry is invalid")
193
+ profiles[profile_id] = profile
194
+ write_team_payload(path, payload)
195
+ return {
196
+ "kind": "team-profile-import",
197
+ "schema_version": TEAM_SCHEMA_VERSION,
198
+ "status": "imported",
199
+ "profile_id": profile_id,
200
+ "path": str(path),
201
+ }
202
+
203
+
204
+ def team_profile_path(project: Path | None = None) -> Path:
205
+ root = (project or Path.cwd()).resolve()
206
+ return root / TEAM_DIR / TEAM_FILE
207
+
208
+
209
+ def default_team_payload() -> dict[str, Any]:
210
+ return {
211
+ "schema_version": TEAM_SCHEMA_VERSION,
212
+ "active_profile": "default",
213
+ "profiles": {
214
+ "default": {
215
+ "description": "Default project team profile",
216
+ "providers": [],
217
+ "sources": [],
218
+ "workflows": ["daily-pr-review"],
219
+ "permissions": {"default_mode": "report-only", "external_writes": "confirm"},
220
+ "local_llm": {"enabled": "personal", "max_context_chars": 6000},
221
+ "prompt_injection": {"external_content_policy": "quote-as-data"},
222
+ "secret_refs": [],
223
+ }
224
+ },
225
+ }
226
+
227
+
228
+ def require_team_payload(project: Path | None = None) -> dict[str, Any]:
229
+ path = team_profile_path(project)
230
+ if not path.exists():
231
+ raise DevKitError("team profile not configured. Run `agent team init` first.")
232
+ return read_team_payload(path)
233
+
234
+
235
+ def require_profile(payload: dict[str, Any], profile_id: str) -> dict[str, Any]:
236
+ profiles = payload.get("profiles") if isinstance(payload.get("profiles"), dict) else {}
237
+ profile = profiles.get(profile_id)
238
+ if not isinstance(profile, dict):
239
+ raise DevKitError(f"team profile not found: {profile_id}")
240
+ return profile
241
+
242
+
243
+ def public_profile(profile_id: str, profile: dict[str, Any], *, detailed: bool = False) -> dict[str, Any]:
244
+ item = {
245
+ "id": profile_id,
246
+ "description": profile.get("description"),
247
+ "providers": list_value(profile.get("providers")),
248
+ "sources": list_value(profile.get("sources")),
249
+ "workflows": list_value(profile.get("workflows")),
250
+ "secret_refs_count": len(list_value(profile.get("secret_refs"))),
251
+ }
252
+ if detailed:
253
+ item["permissions"] = profile.get("permissions") if isinstance(profile.get("permissions"), dict) else {}
254
+ item["local_llm"] = profile.get("local_llm") if isinstance(profile.get("local_llm"), dict) else {}
255
+ item["prompt_injection"] = profile.get("prompt_injection") if isinstance(profile.get("prompt_injection"), dict) else {}
256
+ return item
257
+
258
+
259
+ def list_value(value: Any) -> list[Any]:
260
+ return value if isinstance(value, list) else []
261
+
262
+
263
+ def read_team_payload(path: Path) -> dict[str, Any]:
264
+ text = path.read_text(encoding="utf-8")
265
+ try:
266
+ import yaml # type: ignore
267
+
268
+ payload = yaml.safe_load(text) or {}
269
+ except ImportError:
270
+ payload = read_simple_team_yaml(text)
271
+ if not isinstance(payload, dict):
272
+ raise DevKitError(f"team profile file is invalid: {path}")
273
+ return payload
274
+
275
+
276
+ def write_team_payload(path: Path, payload: dict[str, Any]) -> None:
277
+ try:
278
+ import yaml # type: ignore
279
+
280
+ text = yaml.safe_dump(payload, allow_unicode=True, sort_keys=False)
281
+ except ImportError:
282
+ text = json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
283
+ path.write_text(text, encoding="utf-8")
284
+
285
+
286
+ def read_simple_team_yaml(text: str) -> dict[str, Any]:
287
+ stripped = text.strip()
288
+ if not stripped:
289
+ return {}
290
+ if stripped.startswith("{"):
291
+ data = json.loads(text)
292
+ return data if isinstance(data, dict) else {}
293
+ lines = [
294
+ (len(raw_line) - len(raw_line.lstrip(" ")), raw_line.strip())
295
+ for raw_line in text.splitlines()
296
+ if raw_line.strip() and not raw_line.lstrip().startswith("#")
297
+ ]
298
+ data, index = parse_simple_yaml_block(lines, 0, 0)
299
+ if index < len(lines):
300
+ raise DevKitError("team profile YAML fallback parser could not read the complete file")
301
+ return data if isinstance(data, dict) else {}
302
+
303
+
304
+ def parse_simple_yaml_block(lines: list[tuple[int, str]], index: int, indent: int) -> tuple[Any, int]:
305
+ if index < len(lines) and lines[index][0] == indent and lines[index][1].startswith("- "):
306
+ values: list[Any] = []
307
+ while index < len(lines) and lines[index][0] == indent and lines[index][1].startswith("- "):
308
+ values.append(parse_simple_scalar(lines[index][1][2:].strip()))
309
+ index += 1
310
+ return values, index
311
+
312
+ data: dict[str, Any] = {}
313
+ while index < len(lines):
314
+ line_indent, content = lines[index]
315
+ if line_indent < indent or content.startswith("- "):
316
+ break
317
+ if line_indent > indent:
318
+ raise DevKitError("team profile YAML fallback parser found unexpected indentation")
319
+ if ":" not in content:
320
+ raise DevKitError("team profile YAML fallback parser found an unsupported line")
321
+ key, raw_value = content.split(":", 1)
322
+ key = key.strip()
323
+ raw_value = raw_value.strip()
324
+ index += 1
325
+ if raw_value:
326
+ data[key] = parse_simple_scalar(raw_value)
327
+ continue
328
+ if index >= len(lines) or lines[index][0] < indent:
329
+ data[key] = {}
330
+ continue
331
+ if lines[index][0] == indent and lines[index][1].startswith("- "):
332
+ data[key], index = parse_simple_yaml_block(lines, index, indent)
333
+ continue
334
+ if lines[index][0] <= indent:
335
+ data[key] = {}
336
+ continue
337
+ data[key], index = parse_simple_yaml_block(lines, index, lines[index][0])
338
+ return data, index
339
+
340
+
341
+ def parse_simple_scalar(value: str) -> Any:
342
+ if value == "[]":
343
+ return []
344
+ if value == "{}":
345
+ return {}
346
+ if value in {"true", "false"}:
347
+ return value == "true"
348
+ if value.isdigit():
349
+ return int(value)
350
+ if value.startswith('"') and value.endswith('"'):
351
+ try:
352
+ return json.loads(value)
353
+ except json.JSONDecodeError:
354
+ return value.strip('"')
355
+ if value.startswith("'") and value.endswith("'"):
356
+ return value.strip("'")
357
+ return value
358
+
359
+
360
+ def secret_findings(value: Any, *, path: str = "") -> list[dict[str, Any]]:
361
+ findings: list[dict[str, Any]] = []
362
+ if isinstance(value, dict):
363
+ for key, item in value.items():
364
+ key_path = f"{path}.{key}" if path else str(key)
365
+ if SECRET_KEY_PATTERN.search(str(key)) and not is_reference_key(str(key)) and item not in (None, "", [], {}):
366
+ findings.append({"path": key_path, "reason": "secret-like-key"})
367
+ findings.extend(secret_findings(item, path=key_path))
368
+ return findings
369
+ if isinstance(value, list):
370
+ for index, item in enumerate(value):
371
+ findings.extend(secret_findings(item, path=f"{path}[{index}]"))
372
+ return findings
373
+ if isinstance(value, str) and SECRET_VALUE_PATTERN.search(value):
374
+ findings.append({"path": path, "reason": "secret-like-value"})
375
+ return findings
376
+
377
+
378
+ def is_reference_key(key: str) -> bool:
379
+ normalized = key.lower().replace("-", "_")
380
+ return normalized in {"secret_ref", "secret_refs", "credential_ref", "credential_refs"} or normalized.endswith("_ref") or normalized.endswith("_refs")
@@ -162,12 +162,17 @@ def install_payload(
162
162
 
163
163
  def load_toolchain(root: Path | None = None) -> dict[str, dict[str, Any]]:
164
164
  path = toolchain_path(root)
165
+ if not path.exists():
166
+ return fallback_toolchain()
165
167
  try:
166
168
  import yaml # type: ignore
167
169
  except ImportError:
168
170
  return fallback_toolchain()
169
- with path.open(encoding="utf-8") as file:
170
- data = yaml.safe_load(file) or {}
171
+ try:
172
+ with path.open(encoding="utf-8") as file:
173
+ data = yaml.safe_load(file) or {}
174
+ except OSError:
175
+ return fallback_toolchain()
171
176
  tools = data.get("tools") if isinstance(data, dict) else {}
172
177
  if not isinstance(tools, dict):
173
178
  return {}