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.
- package/README.md +42 -2
- package/package.json +1 -1
- package/runtime/README.md +50 -2
- package/runtime/agent +29 -13
- package/runtime/agents/README.md +3 -0
- package/runtime/agents/github-pr-reviewer/AGENTS.md +10 -0
- package/runtime/agents/github-pr-reviewer/README.md +7 -0
- package/runtime/agents/github-pr-reviewer/agent.yaml +33 -0
- package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/decision-rules.md +8 -0
- package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/workflow.md +9 -0
- package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/decision-rules.md +7 -0
- package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/workflow.md +8 -0
- package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/decision-rules.md +7 -0
- package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/workflow.md +8 -0
- package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/decision-rules.md +9 -0
- package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/workflow.md +10 -0
- package/runtime/agents/github-pr-reviewer/infra/README.md +7 -0
- package/runtime/agents/github-pr-reviewer/knowledge/context.md +7 -0
- package/runtime/agents/github-pr-reviewer/knowledge/system.md +17 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-automation-output.md +10 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-inspection-output.md +9 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-list-output.md +9 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-review-output.md +17 -0
- package/runtime/cli/README.md +37 -1
- package/runtime/cli/aikit/__init__.py +1 -1
- package/runtime/cli/aikit/aliases.py +196 -0
- package/runtime/cli/aikit/app_home.py +78 -0
- package/runtime/cli/aikit/audit.py +344 -0
- package/runtime/cli/aikit/calendar.py +163 -0
- package/runtime/cli/aikit/configuration_orchestrator.py +291 -0
- package/runtime/cli/aikit/decision_store.py +158 -0
- package/runtime/cli/aikit/diagnostics.py +9 -0
- package/runtime/cli/aikit/fallback.py +48 -2
- package/runtime/cli/aikit/github_pr.py +254 -0
- package/runtime/cli/aikit/identity.py +75 -0
- package/runtime/cli/aikit/llm.py +249 -48
- package/runtime/cli/aikit/main.py +1240 -32
- package/runtime/cli/aikit/memory.py +148 -8
- package/runtime/cli/aikit/model_router.py +70 -0
- package/runtime/cli/aikit/notifications.py +9 -0
- package/runtime/cli/aikit/ollama.py +237 -0
- package/runtime/cli/aikit/permissions.py +345 -0
- package/runtime/cli/aikit/personality.py +123 -0
- package/runtime/cli/aikit/provider_wizard.py +19 -0
- package/runtime/cli/aikit/providers.py +4 -5
- package/runtime/cli/aikit/review_gate.py +40 -0
- package/runtime/cli/aikit/scheduler.py +18 -0
- package/runtime/cli/aikit/sessions.py +396 -0
- package/runtime/cli/aikit/setup_wizard.py +12 -0
- package/runtime/cli/aikit/tasks.py +351 -0
- package/runtime/cli/aikit/toolchain.py +274 -0
- package/runtime/providers/calendar.yaml +28 -0
- package/runtime/providers/github.yaml +42 -0
- package/runtime/providers/local-notification.yaml +19 -0
- package/runtime/providers/local-scheduler.yaml +24 -0
- 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
|
-
|
|
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
|
|
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
|
+
}
|