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
@@ -24,13 +24,13 @@ def build_review_gate(
24
24
  if route:
25
25
  required = True
26
26
  reasons.append("deterministic-route")
27
- if model_plan and (model_plan.get("local_llm_selected") or model_plan.get("local_llm_recommended")):
27
+ if model_plan and local_worker_review_required(model_plan):
28
28
  required = True
29
29
  reasons.append("local-llm")
30
30
  if model_plan and model_plan.get("strategy") == "human":
31
31
  required = True
32
32
  reasons.append("human-strategy")
33
- if model_plan and model_plan.get("strategy") == "mini-brain":
33
+ if model_plan and mini_brain_review_required(model_plan):
34
34
  required = True
35
35
  reasons.append("mini-brain")
36
36
  if model_plan and model_plan.get("risk") == "high":
@@ -50,6 +50,18 @@ def build_review_gate(
50
50
  }
51
51
 
52
52
 
53
+ def local_worker_review_required(model_plan: dict[str, Any]) -> bool:
54
+ if not (model_plan.get("local_llm_selected") or model_plan.get("local_llm_recommended")):
55
+ return False
56
+ return model_plan.get("local_llm_provider") == "ollama" or model_plan.get("risk") != "low"
57
+
58
+
59
+ def mini_brain_review_required(model_plan: dict[str, Any]) -> bool:
60
+ if model_plan.get("strategy") != "mini-brain":
61
+ return False
62
+ return model_plan.get("risk") != "low" or model_plan.get("local_llm_provider") == "ollama"
63
+
64
+
53
65
  def mark_reviewed(payload: dict[str, Any], *, reviewer: str | None = None, notes: str | None = None) -> dict[str, Any]:
54
66
  gate = dict(payload)
55
67
  if gate.get("required"):
@@ -29,7 +29,7 @@ def roadmap_payload(root: Path | None = None, *, phase: int | None = None, probl
29
29
  "kind": "roadmap",
30
30
  "schema_version": ROADMAP_SCHEMA_VERSION,
31
31
  "status": "ok",
32
- "version_scope": "v0.2.0",
32
+ "version_scope": "v0.3.0",
33
33
  "source": "cli.aikit.roadmap",
34
34
  "active_problems": active,
35
35
  "preteridos": sorted(preteridos),
@@ -53,10 +53,11 @@ def add_secret_reference(provider: str, key: str, *, env: str | None) -> dict[st
53
53
  raise DevKitError("secrets reference add requires --env VAR_NAME")
54
54
  data = load_secret_references()
55
55
  refs = [item for item in data["references"] if not (item["provider"] == provider and item["key"] == key)]
56
- refs.append({"provider": provider, "key": key, "backend": "env", "env": env, "value_stored": False})
56
+ reference = {"provider": provider, "key": key, "backend": "env", "env": env, "value_stored": False}
57
+ refs.append(reference)
57
58
  data["references"] = sorted(refs, key=lambda item: (item["provider"], item["key"]))
58
59
  save_secret_references(data)
59
- return {"kind": "secret-reference", "status": "saved", "reference": refs[-1], "value_stored": False}
60
+ return {"kind": "secret-reference", "status": "saved", "reference": reference, "value_stored": False}
60
61
 
61
62
 
62
63
  def list_secret_references() -> dict[str, Any]:
@@ -16,6 +16,9 @@ def persist_setup_wizard_payload(
16
16
  wizard = payload.get("setup_wizard")
17
17
  if not isinstance(wizard, dict) or wizard.get("wizard_id"):
18
18
  return payload
19
+ if wizard.get("status") == "denied-by-user":
20
+ payload["next_question"] = wizard.get("next_question")
21
+ return payload
19
22
  persisted = create_provider_wizard(
20
23
  wizard,
21
24
  execution_plan=execution_plan or payload.get("execution_plan"),
@@ -0,0 +1,415 @@
1
+ """Shared memory workspaces with owner-reviewed contributions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import secrets
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from cli.aikit.app_home import app_home, ensure_app_home
12
+ from cli.aikit.errors import DevKitError
13
+ from cli.aikit.knowledge_base import sanitize_snapshot_content, scan_text
14
+ from cli.aikit.prompt_injection import external_content_block
15
+
16
+
17
+ SHARED_MEMORY_SCHEMA_VERSION = "agent-devkit.shared-memory/v1"
18
+
19
+
20
+ def shared_memory_home() -> Path:
21
+ ensure_app_home()
22
+ path = app_home() / "shared-memory"
23
+ path.mkdir(parents=True, exist_ok=True)
24
+ return path
25
+
26
+
27
+ def shared_memory_create(title: str | None = None) -> dict[str, Any]:
28
+ memory_id = slugify(title or "shared-memory")
29
+ root = unique_workspace_path(memory_id)
30
+ for relative in ("incoming", "reviews", "accepted", "rejected", "audit"):
31
+ (root / relative).mkdir(parents=True, exist_ok=True)
32
+ owner_key = new_key("own")
33
+ contributor_key = new_key("contrib")
34
+ manifest = {
35
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
36
+ "id": root.name,
37
+ "title": title or root.name,
38
+ "owner": "local",
39
+ "owner_key": owner_key,
40
+ "contributor_key": contributor_key,
41
+ "share_url": root.as_uri(),
42
+ "created_at": now_iso(),
43
+ "updated_at": now_iso(),
44
+ "policy": {
45
+ "contributors": "key-required",
46
+ "publish": "owner-review-required",
47
+ "accepted_visibility": "readable-by-url-holder",
48
+ },
49
+ }
50
+ write_json(root / "manifest.json", manifest)
51
+ return {
52
+ "kind": "shared-memory",
53
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
54
+ "status": "created",
55
+ "memory": public_manifest(manifest),
56
+ "path": str(root),
57
+ "owner_access": {
58
+ "key": owner_key,
59
+ "role": "owner",
60
+ "usage": "Required to publish reviewed submissions with --yes.",
61
+ },
62
+ "contributor_access": {
63
+ "url": manifest["share_url"],
64
+ "key": contributor_key,
65
+ "role": "contributor",
66
+ },
67
+ "next_steps": [
68
+ "Share contributor_access.url and contributor_access.key with another agent.",
69
+ "Review submissions with `agent memory review <memory-id> <submission-id>`.",
70
+ "Publish approved submissions with `agent memory publish <memory-id> <submission-id> --yes`.",
71
+ ],
72
+ }
73
+
74
+
75
+ def shared_memory_list() -> dict[str, Any]:
76
+ items = []
77
+ for manifest_path in sorted(shared_memory_home().glob("*/manifest.json")):
78
+ try:
79
+ manifest = read_json(manifest_path)
80
+ except DevKitError:
81
+ continue
82
+ items.append(public_manifest(manifest))
83
+ return {
84
+ "kind": "shared-memories",
85
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
86
+ "status": "ok",
87
+ "home": str(shared_memory_home()),
88
+ "items": items,
89
+ }
90
+
91
+
92
+ def shared_memory_status(memory_id: str | None) -> dict[str, Any]:
93
+ root, manifest = require_workspace(memory_id)
94
+ return {
95
+ "kind": "shared-memory",
96
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
97
+ "status": "ok",
98
+ "memory": public_manifest(manifest),
99
+ "submissions": {
100
+ "pending": count_files(root / "incoming"),
101
+ "accepted": count_files(root / "accepted"),
102
+ "rejected": count_files(root / "rejected"),
103
+ },
104
+ "path": str(root),
105
+ }
106
+
107
+
108
+ def shared_memory_read(memory_id: str | None, entry_id: str | None = None, *, contributor_key: str | None = None) -> dict[str, Any]:
109
+ root, manifest = require_workspace(memory_id)
110
+ if contributor_key and contributor_key != manifest.get("contributor_key"):
111
+ return {
112
+ "kind": "shared-memory-read",
113
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
114
+ "status": "blocked",
115
+ "ok": False,
116
+ "reason": "invalid-contributor-key",
117
+ "memory_id": manifest.get("id"),
118
+ "exit_code": 2,
119
+ }
120
+ role = "contributor" if contributor_key else "owner"
121
+ accepted = root / "accepted"
122
+ if entry_id:
123
+ sid = require_id(entry_id, "entry id")
124
+ path = accepted / f"{sid}.md"
125
+ if not path.exists():
126
+ raise DevKitError(f"shared memory accepted entry not found: {sid}")
127
+ content = path.read_text(encoding="utf-8", errors="replace")
128
+ return {
129
+ "kind": "shared-memory-read",
130
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
131
+ "status": "ok",
132
+ "memory_id": manifest.get("id"),
133
+ "role": role,
134
+ "entry_id": sid,
135
+ "path": str(path),
136
+ "content": content,
137
+ "items": [accepted_item(root, path)],
138
+ }
139
+ items = [accepted_item(root, path) for path in sorted(accepted.glob("*.md"))]
140
+ return {
141
+ "kind": "shared-memory-read",
142
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
143
+ "status": "ok",
144
+ "memory_id": manifest.get("id"),
145
+ "role": role,
146
+ "path": str(accepted),
147
+ "count": len(items),
148
+ "items": items,
149
+ }
150
+
151
+
152
+ def shared_memory_submit(
153
+ memory_id: str | None,
154
+ *,
155
+ title: str | None,
156
+ content: str | None,
157
+ contributor_key: str | None,
158
+ ) -> dict[str, Any]:
159
+ root, manifest = require_workspace(memory_id)
160
+ if not contributor_key or contributor_key != manifest.get("contributor_key"):
161
+ return {
162
+ "kind": "shared-memory-submission",
163
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
164
+ "status": "blocked",
165
+ "ok": False,
166
+ "reason": "invalid-contributor-key",
167
+ "memory_id": manifest.get("id"),
168
+ "exit_code": 2,
169
+ }
170
+ if not content:
171
+ raise DevKitError("memory submit requires --content")
172
+ submission_id = unique_submission_id(title or "submission", root / "incoming")
173
+ block = external_content_block("shared-memory-submission", "markdown", content)
174
+ findings = scan_text(content)
175
+ metadata = {
176
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
177
+ "memory_id": manifest.get("id"),
178
+ "submission_id": submission_id,
179
+ "title": title or submission_id,
180
+ "status": "pending",
181
+ "created_at": now_iso(),
182
+ "findings": findings,
183
+ "prompt_injection": {
184
+ "severity": block["severity"],
185
+ "markers": block["detected_injection_markers"],
186
+ },
187
+ }
188
+ (root / "incoming" / f"{submission_id}.md").write_text(sanitize_snapshot_content(content).strip() + "\n", encoding="utf-8")
189
+ write_json(root / "incoming" / f"{submission_id}.json", metadata)
190
+ return {
191
+ "kind": "shared-memory-submission",
192
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
193
+ "status": "pending",
194
+ "ok": True,
195
+ "memory_id": manifest.get("id"),
196
+ "submission_id": submission_id,
197
+ "path": str(root / "incoming" / f"{submission_id}.md"),
198
+ "review_required": True,
199
+ "findings": findings,
200
+ "prompt_injection": metadata["prompt_injection"],
201
+ }
202
+
203
+
204
+ def shared_memory_review(memory_id: str | None, submission_id: str | None, *, persist: bool = True) -> dict[str, Any]:
205
+ root, manifest = require_workspace(memory_id)
206
+ sid = require_id(submission_id, "submission id")
207
+ content_path = root / "incoming" / f"{sid}.md"
208
+ if not content_path.exists():
209
+ raise DevKitError(f"shared memory submission not found: {sid}")
210
+ metadata = read_submission_metadata(root, sid)
211
+ submission_findings = metadata.get("findings") if isinstance(metadata.get("findings"), list) else []
212
+ content = content_path.read_text(encoding="utf-8", errors="replace")
213
+ block = external_content_block(f"shared-memory:{sid}", "markdown", content)
214
+ findings = [*submission_findings]
215
+ if block["severity"] != "none":
216
+ findings.append(
217
+ {
218
+ "reason": "prompt-injection",
219
+ "severity": block["severity"],
220
+ "markers": block["detected_injection_markers"],
221
+ }
222
+ )
223
+ passed = not findings
224
+ review = {
225
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
226
+ "memory_id": manifest.get("id"),
227
+ "submission_id": sid,
228
+ "status": "approved" if passed else "rejected",
229
+ "findings": findings,
230
+ "reviewed_at": now_iso(),
231
+ "prompt_injection": {
232
+ "severity": block["severity"],
233
+ "markers": block["detected_injection_markers"],
234
+ },
235
+ }
236
+ review_path = None
237
+ if persist:
238
+ review_path = root / "reviews" / f"{sid}.json"
239
+ write_json(review_path, review)
240
+ return {
241
+ "kind": "shared-memory-review",
242
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
243
+ "status": review["status"],
244
+ "memory_id": manifest.get("id"),
245
+ "submission_id": sid,
246
+ "review": review,
247
+ "persisted": persist,
248
+ "path": str(review_path) if review_path else None,
249
+ }
250
+
251
+
252
+ def shared_memory_publish(
253
+ memory_id: str | None,
254
+ submission_id: str | None,
255
+ *,
256
+ yes: bool = False,
257
+ owner_key: str | None = None,
258
+ ) -> dict[str, Any]:
259
+ root, manifest = require_workspace(memory_id)
260
+ sid = require_id(submission_id, "submission id")
261
+ if yes and owner_key != manifest.get("owner_key"):
262
+ return {
263
+ "kind": "shared-memory-publish",
264
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
265
+ "status": "blocked",
266
+ "memory_id": manifest.get("id"),
267
+ "submission_id": sid,
268
+ "reason": "owner_key_required",
269
+ "exit_code": 2,
270
+ }
271
+ review = shared_memory_review(manifest.get("id"), sid, persist=yes)
272
+ if review["status"] != "approved":
273
+ return {
274
+ "kind": "shared-memory-publish",
275
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
276
+ "status": "blocked",
277
+ "memory_id": manifest.get("id"),
278
+ "submission_id": sid,
279
+ "review": review,
280
+ "reason": "review-rejected",
281
+ }
282
+ if not yes:
283
+ return {
284
+ "kind": "shared-memory-publish",
285
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
286
+ "status": "planned",
287
+ "memory_id": manifest.get("id"),
288
+ "submission_id": sid,
289
+ "review": review,
290
+ "next_steps": ["Re-run with `--yes --owner-key <owner-key>` to publish into accepted shared memory."],
291
+ }
292
+ source = root / "incoming" / f"{sid}.md"
293
+ target = root / "accepted" / f"{sid}.md"
294
+ target.write_text(source.read_text(encoding="utf-8"), encoding="utf-8")
295
+ source.unlink(missing_ok=True)
296
+ (root / "incoming" / f"{sid}.json").unlink(missing_ok=True)
297
+ manifest["updated_at"] = now_iso()
298
+ write_json(root / "manifest.json", manifest)
299
+ return {
300
+ "kind": "shared-memory-publish",
301
+ "schema_version": SHARED_MEMORY_SCHEMA_VERSION,
302
+ "status": "published",
303
+ "memory_id": manifest.get("id"),
304
+ "submission_id": sid,
305
+ "path": str(target),
306
+ "review": review,
307
+ }
308
+
309
+
310
+ def require_workspace(memory_id: str | None) -> tuple[Path, dict[str, Any]]:
311
+ item_id = require_id(memory_id, "memory id")
312
+ root = shared_memory_home() / item_id
313
+ manifest_path = root / "manifest.json"
314
+ if not manifest_path.exists():
315
+ raise DevKitError(f"shared memory not found: {item_id}")
316
+ return root, read_json(manifest_path)
317
+
318
+
319
+ def public_manifest(manifest: dict[str, Any]) -> dict[str, Any]:
320
+ payload = {
321
+ "id": manifest.get("id"),
322
+ "title": manifest.get("title"),
323
+ "owner": manifest.get("owner"),
324
+ "share_url": manifest.get("share_url"),
325
+ "created_at": manifest.get("created_at"),
326
+ "updated_at": manifest.get("updated_at"),
327
+ "policy": manifest.get("policy") or {},
328
+ "contributor_key_available": bool(manifest.get("contributor_key")),
329
+ }
330
+ return payload
331
+
332
+
333
+ def accepted_item(root: Path, path: Path) -> dict[str, Any]:
334
+ text = path.read_text(encoding="utf-8", errors="replace")
335
+ return {
336
+ "id": path.stem,
337
+ "title": title_for(path, text),
338
+ "path": str(path.relative_to(root)),
339
+ "bytes": path.stat().st_size,
340
+ }
341
+
342
+
343
+ def title_for(path: Path, text: str) -> str:
344
+ for line in text.splitlines():
345
+ stripped = line.strip()
346
+ if stripped.startswith("#"):
347
+ return stripped.lstrip("#").strip() or path.stem
348
+ return path.stem
349
+
350
+
351
+ def unique_workspace_path(base_id: str) -> Path:
352
+ home = shared_memory_home()
353
+ candidate = home / base_id
354
+ if not candidate.exists():
355
+ return candidate
356
+ index = 2
357
+ while (home / f"{base_id}-{index}").exists():
358
+ index += 1
359
+ return home / f"{base_id}-{index}"
360
+
361
+
362
+ def unique_submission_id(title: str, folder: Path) -> str:
363
+ base = slugify(title)
364
+ candidate = base
365
+ index = 2
366
+ while (folder / f"{candidate}.md").exists() or (folder / f"{candidate}.json").exists():
367
+ candidate = f"{base}-{index}"
368
+ index += 1
369
+ return candidate
370
+
371
+
372
+ def require_id(value: str | None, label: str) -> str:
373
+ item_id = slugify(value or "")
374
+ if not item_id:
375
+ raise DevKitError(f"{label} is required")
376
+ return item_id
377
+
378
+
379
+ def slugify(value: str) -> str:
380
+ import re
381
+
382
+ slug = re.sub(r"[^a-zA-Z0-9]+", "-", str(value).strip().lower()).strip("-")
383
+ return slug
384
+
385
+
386
+ def new_key(prefix: str) -> str:
387
+ return f"{prefix}_{secrets.token_urlsafe(24)}"
388
+
389
+
390
+ def now_iso() -> str:
391
+ return datetime.now(timezone.utc).isoformat()
392
+
393
+
394
+ def count_files(path: Path) -> int:
395
+ return len([item for item in path.glob("*.md") if item.is_file()])
396
+
397
+
398
+ def read_json(path: Path) -> dict[str, Any]:
399
+ try:
400
+ payload = json.loads(path.read_text(encoding="utf-8"))
401
+ except (OSError, json.JSONDecodeError) as exc:
402
+ raise DevKitError(f"invalid shared memory file: {path}") from exc
403
+ return payload if isinstance(payload, dict) else {}
404
+
405
+
406
+ def read_submission_metadata(root: Path, submission_id: str) -> dict[str, Any]:
407
+ path = root / "incoming" / f"{submission_id}.json"
408
+ if not path.exists():
409
+ return {}
410
+ return read_json(path)
411
+
412
+
413
+ def write_json(path: Path, payload: dict[str, Any]) -> None:
414
+ path.parent.mkdir(parents=True, exist_ok=True)
415
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
@@ -0,0 +1,152 @@
1
+ """Specialist agent readiness summaries for onboarding and doctor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter, defaultdict
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from cli.aikit.agent_registry import load_agent_registry
10
+ from cli.aikit.sources import list_sources, source_status
11
+
12
+
13
+ SPECIALIST_READINESS_SCHEMA_VERSION = "agent-devkit.specialist-readiness/v1"
14
+
15
+
16
+ def specialist_readiness(root: Path) -> dict[str, Any]:
17
+ registry = load_agent_registry(root)
18
+ capabilities = registry.get("capabilities") if isinstance(registry.get("capabilities"), dict) else {}
19
+ agents = registry.get("agents") if isinstance(registry.get("agents"), dict) else {}
20
+ source_summary = configured_source_summary()
21
+ configured_providers = set(source_summary["configured_providers"])
22
+ provider_capabilities: Counter[str] = Counter()
23
+ agent_providers: dict[str, Counter[str]] = defaultdict(Counter)
24
+ agent_capabilities: Counter[str] = Counter()
25
+ source_enabled_capabilities: Counter[str] = Counter()
26
+
27
+ for capability in capabilities.values():
28
+ if not isinstance(capability, dict):
29
+ continue
30
+ agent_id = str(capability.get("agent_id") or "")
31
+ if not agent_id:
32
+ continue
33
+ provider = str(capability.get("provider") or "").strip()
34
+ if provider:
35
+ provider_capabilities[provider] += 1
36
+ agent_providers[agent_id][provider] += 1
37
+ agent_capabilities[agent_id] += 1
38
+ source_contract = capability.get("source_contract") if isinstance(capability.get("source_contract"), dict) else {}
39
+ if source_contract.get("enabled") or source_contract.get("supported"):
40
+ source_enabled_capabilities[agent_id] += 1
41
+
42
+ items = [
43
+ agent_readiness_item(
44
+ agent_id,
45
+ agents.get(agent_id) if isinstance(agents.get(agent_id), dict) else {},
46
+ providers,
47
+ configured_providers=configured_providers,
48
+ source_enabled_count=source_enabled_capabilities.get(agent_id, 0),
49
+ )
50
+ for agent_id, providers in sorted(agent_providers.items())
51
+ ]
52
+ missing_provider_counts: Counter[str] = Counter()
53
+ for item in items:
54
+ missing_provider_counts.update(item["missing_providers"])
55
+
56
+ blocked = [item for item in items if item["status"] == "needs-setup"]
57
+ partial = [item for item in items if item["status"] == "partial"]
58
+ ready = [item for item in items if item["status"] == "ready"]
59
+ status = "ready"
60
+ if blocked:
61
+ status = "needs-setup"
62
+ elif partial:
63
+ status = "partial"
64
+ return {
65
+ "kind": "specialist-readiness",
66
+ "schema_version": SPECIALIST_READINESS_SCHEMA_VERSION,
67
+ "status": status,
68
+ "agents_total": len(agents),
69
+ "capabilities_total": len(capabilities),
70
+ "agents_with_provider_requirements": len(items),
71
+ "ready_agents": len(ready),
72
+ "partial_agents": len(partial),
73
+ "needs_setup_agents": len(blocked),
74
+ "providers_required": [
75
+ {"id": provider, "capabilities": count, "configured": provider in configured_providers}
76
+ for provider, count in provider_capabilities.most_common()
77
+ ],
78
+ "configured_providers": sorted(configured_providers),
79
+ "missing_providers": [
80
+ {"id": provider, "agents": count}
81
+ for provider, count in missing_provider_counts.most_common()
82
+ ],
83
+ "items": items,
84
+ "source_summary": source_summary,
85
+ "next_steps": readiness_next_steps(missing_provider_counts),
86
+ }
87
+
88
+
89
+ def agent_readiness_item(
90
+ agent_id: str,
91
+ agent: dict[str, Any],
92
+ providers: Counter[str],
93
+ *,
94
+ configured_providers: set[str],
95
+ source_enabled_count: int,
96
+ ) -> dict[str, Any]:
97
+ required = sorted(providers)
98
+ configured = sorted(provider for provider in required if provider in configured_providers)
99
+ missing = sorted(provider for provider in required if provider not in configured_providers)
100
+ status = "ready"
101
+ if missing and configured:
102
+ status = "partial"
103
+ elif missing:
104
+ status = "needs-setup"
105
+ return {
106
+ "id": agent_id,
107
+ "name": agent.get("name") or agent_id,
108
+ "status": status,
109
+ "required_providers": required,
110
+ "configured_providers": configured,
111
+ "missing_providers": missing,
112
+ "provider_capabilities": dict(sorted(providers.items())),
113
+ "source_enabled_capabilities": source_enabled_count,
114
+ "setup_commands": [f"agent provider configure {provider}" for provider in missing[:3]],
115
+ }
116
+
117
+
118
+ def configured_source_summary() -> dict[str, Any]:
119
+ sources = list_sources()
120
+ try:
121
+ status = source_status()
122
+ status_items = status.get("items") if isinstance(status.get("items"), list) else []
123
+ except Exception: # noqa: BLE001 - readiness must remain diagnostic, not fatal.
124
+ status = {"status": "missing", "items": []}
125
+ status_items = []
126
+ configured_providers = sorted(
127
+ {
128
+ str(item.get("provider"))
129
+ for item in status_items
130
+ if isinstance(item, dict) and item.get("status") == "ok" and item.get("provider")
131
+ }
132
+ )
133
+ providers_with_sources = sorted(
134
+ {
135
+ str(item.get("provider"))
136
+ for item in sources.get("items") or []
137
+ if isinstance(item, dict) and item.get("provider")
138
+ }
139
+ )
140
+ return {
141
+ "status": status.get("status"),
142
+ "sources_count": len(sources.get("items") or []),
143
+ "configured_providers": configured_providers,
144
+ "providers_with_sources": providers_with_sources,
145
+ "stored_secret": sources.get("stored_secret") is True or status.get("stored_secret") is True,
146
+ }
147
+
148
+
149
+ def readiness_next_steps(missing_provider_counts: Counter[str]) -> list[str]:
150
+ if not missing_provider_counts:
151
+ return []
152
+ return [f"agent provider configure {provider}" for provider, _ in missing_provider_counts.most_common(5)]