agent-devkit 0.0.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +42 -2
  2. package/package.json +1 -1
  3. package/runtime/README.md +50 -2
  4. package/runtime/agent +29 -13
  5. package/runtime/agents/README.md +3 -0
  6. package/runtime/agents/github-pr-reviewer/AGENTS.md +10 -0
  7. package/runtime/agents/github-pr-reviewer/README.md +7 -0
  8. package/runtime/agents/github-pr-reviewer/agent.yaml +33 -0
  9. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/capability.yaml +20 -0
  10. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/decision-rules.md +8 -0
  11. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/workflow.md +9 -0
  12. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/capability.yaml +20 -0
  13. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/decision-rules.md +7 -0
  14. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/workflow.md +8 -0
  15. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/capability.yaml +20 -0
  16. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/decision-rules.md +7 -0
  17. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/workflow.md +8 -0
  18. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/capability.yaml +20 -0
  19. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/decision-rules.md +9 -0
  20. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/workflow.md +10 -0
  21. package/runtime/agents/github-pr-reviewer/infra/README.md +7 -0
  22. package/runtime/agents/github-pr-reviewer/knowledge/context.md +7 -0
  23. package/runtime/agents/github-pr-reviewer/knowledge/system.md +17 -0
  24. package/runtime/agents/github-pr-reviewer/templates/pr-automation-output.md +10 -0
  25. package/runtime/agents/github-pr-reviewer/templates/pr-inspection-output.md +9 -0
  26. package/runtime/agents/github-pr-reviewer/templates/pr-list-output.md +9 -0
  27. package/runtime/agents/github-pr-reviewer/templates/pr-review-output.md +17 -0
  28. package/runtime/cli/README.md +37 -1
  29. package/runtime/cli/aikit/__init__.py +1 -1
  30. package/runtime/cli/aikit/aliases.py +196 -0
  31. package/runtime/cli/aikit/app_home.py +78 -0
  32. package/runtime/cli/aikit/audit.py +344 -0
  33. package/runtime/cli/aikit/calendar.py +163 -0
  34. package/runtime/cli/aikit/configuration_orchestrator.py +291 -0
  35. package/runtime/cli/aikit/decision_store.py +158 -0
  36. package/runtime/cli/aikit/diagnostics.py +9 -0
  37. package/runtime/cli/aikit/fallback.py +48 -2
  38. package/runtime/cli/aikit/github_pr.py +254 -0
  39. package/runtime/cli/aikit/identity.py +75 -0
  40. package/runtime/cli/aikit/llm.py +249 -48
  41. package/runtime/cli/aikit/main.py +1240 -32
  42. package/runtime/cli/aikit/memory.py +148 -8
  43. package/runtime/cli/aikit/model_router.py +70 -0
  44. package/runtime/cli/aikit/notifications.py +9 -0
  45. package/runtime/cli/aikit/ollama.py +237 -0
  46. package/runtime/cli/aikit/permissions.py +345 -0
  47. package/runtime/cli/aikit/personality.py +123 -0
  48. package/runtime/cli/aikit/provider_wizard.py +19 -0
  49. package/runtime/cli/aikit/providers.py +4 -5
  50. package/runtime/cli/aikit/review_gate.py +40 -0
  51. package/runtime/cli/aikit/scheduler.py +18 -0
  52. package/runtime/cli/aikit/sessions.py +396 -0
  53. package/runtime/cli/aikit/setup_wizard.py +12 -0
  54. package/runtime/cli/aikit/tasks.py +351 -0
  55. package/runtime/cli/aikit/toolchain.py +274 -0
  56. package/runtime/providers/calendar.yaml +28 -0
  57. package/runtime/providers/github.yaml +42 -0
  58. package/runtime/providers/local-notification.yaml +19 -0
  59. package/runtime/providers/local-scheduler.yaml +24 -0
  60. package/runtime/vendor/skills/napkin/napkin.md +13 -3
@@ -0,0 +1,345 @@
1
+ """Local permission policy helpers for Agent DevKit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from cli.aikit.app_home import ensure_app_home, policies_home
11
+ from cli.aikit.memory import redact_secrets
12
+
13
+
14
+ PERMISSION_LEVELS = (
15
+ "read-only",
16
+ "draft-only",
17
+ "comment-with-approval",
18
+ "write-with-approval",
19
+ "auto-write",
20
+ "admin",
21
+ )
22
+ DEFAULT_PERMISSION_LEVEL = "read-only"
23
+ PROVIDER_BY_AGENT = {
24
+ "github-pr-reviewer": "github",
25
+ }
26
+ ACTION_REQUIRED_LEVEL = {
27
+ "read": "read-only",
28
+ "draft": "draft-only",
29
+ "comment": "comment-with-approval",
30
+ "write": "write-with-approval",
31
+ "approve": "write-with-approval",
32
+ "request-changes": "write-with-approval",
33
+ "admin": "admin",
34
+ }
35
+
36
+
37
+ def now_iso() -> str:
38
+ return datetime.now(timezone.utc).isoformat()
39
+
40
+
41
+ def ensure_policies_home() -> Path:
42
+ ensure_app_home()
43
+ home = policies_home()
44
+ home.mkdir(parents=True, exist_ok=True)
45
+ return home
46
+
47
+
48
+ def permissions_json_path() -> Path:
49
+ return ensure_policies_home() / "permissions.json"
50
+
51
+
52
+ def permissions_md_path() -> Path:
53
+ return ensure_policies_home() / "permissions.md"
54
+
55
+
56
+ def empty_policy() -> dict[str, Any]:
57
+ return {
58
+ "version": 1,
59
+ "default_level": DEFAULT_PERMISSION_LEVEL,
60
+ "grants": [],
61
+ }
62
+
63
+
64
+ def load_permissions() -> dict[str, Any]:
65
+ path = permissions_json_path()
66
+ if not path.exists():
67
+ policy = empty_policy()
68
+ save_permissions(policy)
69
+ return policy
70
+ try:
71
+ data = json.loads(path.read_text(encoding="utf-8"))
72
+ except json.JSONDecodeError:
73
+ backup = path.with_suffix(f".corrupt-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}.json")
74
+ path.replace(backup)
75
+ data = empty_policy()
76
+ if not isinstance(data, dict):
77
+ data = empty_policy()
78
+ data.setdefault("version", 1)
79
+ data.setdefault("default_level", DEFAULT_PERMISSION_LEVEL)
80
+ if data.get("default_level") not in PERMISSION_LEVELS:
81
+ data["default_level"] = DEFAULT_PERMISSION_LEVEL
82
+ if not isinstance(data.get("grants"), list):
83
+ data["grants"] = []
84
+ return data
85
+
86
+
87
+ def save_permissions(policy: dict[str, Any]) -> Path:
88
+ ensure_policies_home()
89
+ path = permissions_json_path()
90
+ path.write_text(json.dumps(policy, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
91
+ permissions_md_path().write_text(render_permissions_md(policy), encoding="utf-8")
92
+ return path
93
+
94
+
95
+ def show_permissions() -> dict[str, Any]:
96
+ policy = load_permissions()
97
+ return {
98
+ "kind": "permissions",
99
+ "status": "ok",
100
+ "default_level": policy["default_level"],
101
+ "levels": list(PERMISSION_LEVELS),
102
+ "grants": [public_grant(item) for item in policy["grants"] if isinstance(item, dict)],
103
+ "json_path": str(permissions_json_path()),
104
+ "markdown_path": str(permissions_md_path()),
105
+ }
106
+
107
+
108
+ def grant_permission(
109
+ agent: str,
110
+ provider: str,
111
+ level: str,
112
+ *,
113
+ project: str | None = None,
114
+ task_id: str | None = None,
115
+ ) -> dict[str, Any]:
116
+ validate_level(level)
117
+ policy = load_permissions()
118
+ grants = [item for item in policy["grants"] if isinstance(item, dict)]
119
+ grant = find_grant(grants, agent=agent, provider=provider, project=project, task_id=task_id)
120
+ timestamp = now_iso()
121
+ if grant:
122
+ grant["level"] = level
123
+ grant["updated_at"] = timestamp
124
+ else:
125
+ grant = {
126
+ "agent": agent,
127
+ "provider": provider,
128
+ "level": level,
129
+ "project": project,
130
+ "task_id": task_id,
131
+ "created_at": timestamp,
132
+ "updated_at": timestamp,
133
+ }
134
+ grants.append(grant)
135
+ policy["grants"] = grants
136
+ path = save_permissions(policy)
137
+ return {
138
+ "kind": "permissions",
139
+ "status": "granted",
140
+ "grant": public_grant(grant),
141
+ "json_path": str(path),
142
+ "markdown_path": str(permissions_md_path()),
143
+ }
144
+
145
+
146
+ def revoke_permission(
147
+ agent: str,
148
+ provider: str,
149
+ level: str | None = None,
150
+ *,
151
+ project: str | None = None,
152
+ task_id: str | None = None,
153
+ ) -> dict[str, Any]:
154
+ if level:
155
+ validate_level(level)
156
+ policy = load_permissions()
157
+ removed: list[dict[str, Any]] = []
158
+ kept: list[dict[str, Any]] = []
159
+ for item in policy["grants"]:
160
+ if not isinstance(item, dict):
161
+ continue
162
+ matches = (
163
+ item.get("agent") == agent
164
+ and item.get("provider") == provider
165
+ and item.get("project") == project
166
+ and item.get("task_id") == task_id
167
+ and (level is None or item.get("level") == level)
168
+ )
169
+ if matches:
170
+ removed.append(item)
171
+ else:
172
+ kept.append(item)
173
+ policy["grants"] = kept
174
+ path = save_permissions(policy)
175
+ return {
176
+ "kind": "permissions",
177
+ "status": "revoked" if removed else "not-found",
178
+ "removed": [public_grant(item) for item in removed],
179
+ "json_path": str(path),
180
+ "markdown_path": str(permissions_md_path()),
181
+ }
182
+
183
+
184
+ def permission_check(
185
+ *,
186
+ agent: str | None,
187
+ provider: str | None,
188
+ action: str,
189
+ project: str | None = None,
190
+ task_id: str | None = None,
191
+ ) -> dict[str, Any]:
192
+ required = required_level_for_action(action)
193
+ resolved_provider = provider or provider_for_agent(agent)
194
+ policy = load_permissions()
195
+ grant = best_grant(
196
+ policy["grants"],
197
+ agent=agent,
198
+ provider=resolved_provider,
199
+ project=project,
200
+ task_id=task_id,
201
+ )
202
+ granted = normalize_level(grant.get("level") if grant else policy.get("default_level", DEFAULT_PERMISSION_LEVEL))
203
+ allowed = level_allows(granted, required)
204
+ return {
205
+ "kind": "permission-check",
206
+ "status": "allowed" if allowed else "blocked",
207
+ "ok": allowed,
208
+ "agent": agent,
209
+ "provider": resolved_provider,
210
+ "action": action,
211
+ "required_level": required,
212
+ "granted_level": granted,
213
+ "grant": public_grant(grant) if grant else None,
214
+ "default_level": policy.get("default_level", DEFAULT_PERMISSION_LEVEL),
215
+ "requires_permission": not allowed,
216
+ }
217
+
218
+
219
+ def assert_permission(
220
+ *,
221
+ agent: str | None,
222
+ provider: str | None,
223
+ action: str,
224
+ project: str | None = None,
225
+ task_id: str | None = None,
226
+ ) -> dict[str, Any]:
227
+ return permission_check(agent=agent, provider=provider, action=action, project=project, task_id=task_id)
228
+
229
+
230
+ def provider_for_agent(agent: str | None) -> str | None:
231
+ return PROVIDER_BY_AGENT.get(agent or "")
232
+
233
+
234
+ def required_level_for_action(action: str) -> str:
235
+ return ACTION_REQUIRED_LEVEL.get(action, action if action in PERMISSION_LEVELS else "write-with-approval")
236
+
237
+
238
+ def normalize_level(level: Any) -> str:
239
+ return str(level) if str(level) in PERMISSION_LEVELS else DEFAULT_PERMISSION_LEVEL
240
+
241
+
242
+ def level_allows(granted: str, required: str) -> bool:
243
+ validate_level(granted)
244
+ validate_level(required)
245
+ return PERMISSION_LEVELS.index(granted) >= PERMISSION_LEVELS.index(required)
246
+
247
+
248
+ def validate_level(level: str) -> None:
249
+ if level not in PERMISSION_LEVELS:
250
+ available = ", ".join(PERMISSION_LEVELS)
251
+ raise ValueError(f"unknown permission level: {level}. available: {available}")
252
+
253
+
254
+ def best_grant(
255
+ grants: list[Any],
256
+ *,
257
+ agent: str | None,
258
+ provider: str | None,
259
+ project: str | None,
260
+ task_id: str | None,
261
+ ) -> dict[str, Any] | None:
262
+ candidates = [
263
+ item
264
+ for item in grants
265
+ if isinstance(item, dict)
266
+ and item.get("agent") == agent
267
+ and item.get("provider") == provider
268
+ and item.get("project") in {None, project}
269
+ and item.get("task_id") in {None, task_id}
270
+ ]
271
+ if not candidates:
272
+ return None
273
+ candidates.sort(key=grant_specificity, reverse=True)
274
+ return candidates[0]
275
+
276
+
277
+ def find_grant(
278
+ grants: list[dict[str, Any]],
279
+ *,
280
+ agent: str,
281
+ provider: str,
282
+ project: str | None,
283
+ task_id: str | None,
284
+ ) -> dict[str, Any] | None:
285
+ for item in grants:
286
+ if (
287
+ item.get("agent") == agent
288
+ and item.get("provider") == provider
289
+ and item.get("project") == project
290
+ and item.get("task_id") == task_id
291
+ ):
292
+ return item
293
+ return None
294
+
295
+
296
+ def grant_specificity(grant: dict[str, Any]) -> tuple[int, int]:
297
+ scope = int(bool(grant.get("project"))) + int(bool(grant.get("task_id")))
298
+ return scope, PERMISSION_LEVELS.index(normalize_level(grant.get("level")))
299
+
300
+
301
+ def public_grant(grant: dict[str, Any] | None) -> dict[str, Any] | None:
302
+ if not grant:
303
+ return None
304
+ return {
305
+ "agent": grant.get("agent"),
306
+ "provider": grant.get("provider"),
307
+ "level": grant.get("level"),
308
+ "project": grant.get("project"),
309
+ "task_id": grant.get("task_id"),
310
+ "created_at": grant.get("created_at"),
311
+ "updated_at": grant.get("updated_at"),
312
+ }
313
+
314
+
315
+ def render_permissions_md(policy: dict[str, Any]) -> str:
316
+ lines = [
317
+ "# Agent DevKit Permissions",
318
+ "",
319
+ f"- Default level: `{redact_secrets(str(policy.get('default_level') or DEFAULT_PERMISSION_LEVEL))}`",
320
+ "",
321
+ "## Levels",
322
+ "",
323
+ ]
324
+ for level in PERMISSION_LEVELS:
325
+ lines.append(f"- `{level}`")
326
+ lines.extend(["", "## Grants", ""])
327
+ grants = [item for item in policy.get("grants", []) if isinstance(item, dict)]
328
+ if not grants:
329
+ lines.append("- No explicit grants configured.")
330
+ for item in grants:
331
+ scope = []
332
+ if item.get("project"):
333
+ scope.append(f"project={item['project']}")
334
+ if item.get("task_id"):
335
+ scope.append(f"task={item['task_id']}")
336
+ scope_text = f" ({', '.join(scope)})" if scope else ""
337
+ lines.append(
338
+ "- "
339
+ f"`{redact_secrets(str(item.get('agent') or '-'))}` / "
340
+ f"`{redact_secrets(str(item.get('provider') or '-'))}` -> "
341
+ f"`{redact_secrets(str(item.get('level') or DEFAULT_PERMISSION_LEVEL))}`"
342
+ f"{scope_text}"
343
+ )
344
+ lines.append("")
345
+ return "\n".join(lines)
@@ -0,0 +1,123 @@
1
+ """Local Agent DevKit personality persistence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from cli.aikit.identity import DEFAULT_PUBLIC_NAME
10
+ from cli.aikit.memory import MEMORY_FILE_TEMPLATES, ensure_memory, memory_home
11
+
12
+
13
+ FIELD_LABELS = {
14
+ "agent_name": "Name",
15
+ "user_name": "User name",
16
+ "language": "Language",
17
+ "tone": "Tone",
18
+ "detail_level": "Detail level",
19
+ }
20
+
21
+
22
+ def personality_path() -> Path:
23
+ ensure_memory()
24
+ return memory_home() / "personality.md"
25
+
26
+
27
+ def load_personality() -> dict[str, Any]:
28
+ path = personality_path()
29
+ text = path.read_text(encoding="utf-8") if path.exists() else ""
30
+ return {
31
+ "kind": "personality",
32
+ "status": "ok",
33
+ "path": str(path),
34
+ "agent_name": parse_field(text, FIELD_LABELS["agent_name"]) or DEFAULT_PUBLIC_NAME,
35
+ "user_name": parse_field(text, FIELD_LABELS["user_name"]),
36
+ "language": parse_field(text, FIELD_LABELS["language"]),
37
+ "tone": parse_field(text, FIELD_LABELS["tone"]) or "direct",
38
+ "detail_level": parse_field(text, FIELD_LABELS["detail_level"]) or "concise",
39
+ "questions": setup_questions(),
40
+ }
41
+
42
+
43
+ def update_personality(
44
+ *,
45
+ agent_name: str | None = None,
46
+ user_name: str | None = None,
47
+ language: str | None = None,
48
+ tone: str | None = None,
49
+ detail_level: str | None = None,
50
+ ) -> dict[str, Any]:
51
+ current = load_personality()
52
+ values = {
53
+ "agent_name": clean_field(agent_name) or current.get("agent_name") or DEFAULT_PUBLIC_NAME,
54
+ "user_name": clean_field(user_name) if user_name is not None else current.get("user_name"),
55
+ "language": clean_field(language) if language is not None else current.get("language"),
56
+ "tone": clean_field(tone) or current.get("tone") or "direct",
57
+ "detail_level": clean_field(detail_level) or current.get("detail_level") or "concise",
58
+ }
59
+ path = personality_path()
60
+ path.write_text(render_personality(values), encoding="utf-8")
61
+ payload = load_personality()
62
+ payload["status"] = "updated"
63
+ return payload
64
+
65
+
66
+ def reset_personality() -> dict[str, Any]:
67
+ path = personality_path()
68
+ path.write_text(MEMORY_FILE_TEMPLATES["personality.md"], encoding="utf-8")
69
+ payload = load_personality()
70
+ payload["status"] = "reset"
71
+ return payload
72
+
73
+
74
+ def setup_personality() -> dict[str, Any]:
75
+ payload = load_personality()
76
+ payload["status"] = "needs-input"
77
+ payload["message"] = "Use `agent personality edit --name <nome>` to configure personality non-interactively."
78
+ return payload
79
+
80
+
81
+ def render_personality(values: dict[str, Any]) -> str:
82
+ return f"""# Personality
83
+
84
+ Configured public identity and response style for Agent DevKit.
85
+
86
+ ## Agent
87
+
88
+ - Name: {values.get("agent_name") or DEFAULT_PUBLIC_NAME}
89
+ - User name: {values.get("user_name") or ""}
90
+ - Language: {values.get("language") or ""}
91
+
92
+ ## Style
93
+
94
+ - Tone: {values.get("tone") or "direct"}
95
+ - Detail level: {values.get("detail_level") or "concise"}
96
+ """
97
+
98
+
99
+ def parse_field(text: str, label: str) -> str | None:
100
+ pattern = re.compile(rf"(?im)^[ \t]*-[ \t]*{re.escape(label)}[ \t]*:[ \t]*([^\r\n]*)[ \t]*$")
101
+ match = pattern.search(text)
102
+ if not match:
103
+ return None
104
+ value = match.group(1).strip()
105
+ return value or None
106
+
107
+
108
+ def clean_field(value: str | None) -> str | None:
109
+ if value is None:
110
+ return None
111
+ cleaned = " ".join(str(value).split())
112
+ return cleaned or None
113
+
114
+
115
+ def setup_questions() -> list[str]:
116
+ return [
117
+ "Como voce deseja que eu me chame?",
118
+ "Deseja criar um comando com esse nome para me chamar diretamente?",
119
+ "Como voce se chama?",
120
+ "Em qual idioma devo responder por padrao?",
121
+ "Voce prefere respostas curtas, detalhadas, criticas ou didaticas?",
122
+ "Qual backend LLM voce prefere como primario?",
123
+ ]
@@ -0,0 +1,19 @@
1
+ """Backward-compatible provider wizard wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from cli.aikit.configuration_orchestrator import provider_setup_wizard
9
+
10
+
11
+ def missing_source_wizard(prompt: str, route: dict[str, Any], *, root: Path | None = None) -> dict[str, Any]:
12
+ provider = str(route.get("provider") or "")
13
+ return provider_setup_wizard(
14
+ root or Path(__file__).resolve().parents[2],
15
+ provider,
16
+ prompt=prompt,
17
+ route=route,
18
+ reason="No reusable source is configured for this routed prompt.",
19
+ )
@@ -8,6 +8,7 @@ import re
8
8
  from pathlib import Path
9
9
  from typing import Any
10
10
 
11
+ from cli.aikit.app_home import app_home, config_path as app_config_path, ensure_app_home
11
12
  from cli.aikit.credentials import CredentialResolverError, resolve_provider_credentials
12
13
 
13
14
 
@@ -454,14 +455,11 @@ def load_provider_config(provider_id: str) -> dict[str, Any]:
454
455
 
455
456
 
456
457
  def runtime_config_home() -> Path:
457
- raw = os.environ.get("AIKIT_CONFIG_HOME") or os.environ.get("AI_DEVKIT_CONFIG_HOME")
458
- if raw:
459
- return Path(raw).expanduser().resolve()
460
- return (Path.home() / ".ai-devkit").resolve()
458
+ return app_home()
461
459
 
462
460
 
463
461
  def runtime_config_path() -> Path:
464
- return runtime_config_home() / "config.json"
462
+ return app_config_path()
465
463
 
466
464
 
467
465
  def load_runtime_config() -> dict[str, Any]:
@@ -479,6 +477,7 @@ def load_runtime_config() -> dict[str, Any]:
479
477
 
480
478
 
481
479
  def save_runtime_config(config: dict[str, Any]) -> Path:
480
+ ensure_app_home()
482
481
  path = runtime_config_path()
483
482
  path.parent.mkdir(parents=True, exist_ok=True)
484
483
  path.write_text(json.dumps(config, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
@@ -0,0 +1,40 @@
1
+ """Mandatory review gate decisions before completing agentic work."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+
9
+ REVIEW_REQUIRED_PATTERN = re.compile(
10
+ r"(?i)\b(c[oó]digo|software|documento|spec|especifica|requisit|plano|automac|pr|pull request|deploy|infra|sql|banco|seguran)\b"
11
+ )
12
+
13
+
14
+ def build_review_gate(prompt: str, *, route: dict[str, Any] | None = None, model_plan: dict[str, Any] | None = None) -> dict[str, Any]:
15
+ required = bool(REVIEW_REQUIRED_PATTERN.search(prompt))
16
+ if route:
17
+ required = True
18
+ if model_plan and (model_plan.get("local_llm_selected") or model_plan.get("local_llm_recommended")):
19
+ required = True
20
+ return {
21
+ "kind": "review-gate",
22
+ "required": required,
23
+ "status": "pending" if required else "not-required",
24
+ "preferred_reviewers": ["claude-code", "codex-cli"],
25
+ "reason": (
26
+ "Deliverable or delegated local-LLM work requires coordinator review before completion."
27
+ if required
28
+ else "Prompt does not require a formal review gate."
29
+ ),
30
+ "route": route,
31
+ }
32
+
33
+
34
+ def mark_reviewed(payload: dict[str, Any], *, reviewer: str | None = None, notes: str | None = None) -> dict[str, Any]:
35
+ gate = dict(payload)
36
+ if gate.get("required"):
37
+ gate["status"] = "reviewed"
38
+ gate["reviewer"] = reviewer or "coordinator"
39
+ gate["notes"] = notes or "Reviewed by the active coordinator before final response."
40
+ return gate
@@ -0,0 +1,18 @@
1
+ """Local scheduler entrypoints for Agent DevKit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from cli.aikit.tasks import scheduler_run_once
6
+
7
+
8
+ def run_scheduler_once(*, dry_run: bool = False) -> dict:
9
+ return scheduler_run_once(dry_run=dry_run)
10
+
11
+
12
+ def scheduler_daemon_plan() -> dict:
13
+ return {
14
+ "kind": "scheduler",
15
+ "status": "planned",
16
+ "daemon": True,
17
+ "message": "Scheduler daemon is not started by the MVP CLI. Use `agent scheduler run-once` from cron/systemd/launchd.",
18
+ }