@jx0/agency 0.2.1 → 0.4.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 (62) hide show
  1. package/README.md +120 -52
  2. package/dashboard/out/404.html +1 -1
  3. package/dashboard/out/_next/static/chunks/app/_not-found/{page-ad40673d821037f6.js → page-5cb94002960ab71a.js} +1 -1
  4. package/dashboard/out/_next/static/chunks/app/layout-6249f74085ad56b1.js +1 -0
  5. package/dashboard/out/_next/static/chunks/app/page-0a5ee03ddf4553ab.js +1 -0
  6. package/dashboard/out/_next/static/chunks/{main-app-1d848b791b823fa6.js → main-app-0398d52862f5c730.js} +1 -1
  7. package/dashboard/out/_next/static/css/a13af72b10a7d74f.css +1 -0
  8. package/dashboard/out/index.html +1 -1
  9. package/dashboard/out/index.txt +4 -4
  10. package/docs/images/agency_cli_ps.png +0 -0
  11. package/docs/images/agency_ui_ai_prodivder_settings.png +0 -0
  12. package/docs/images/agency_ui_aws_settings.png +0 -0
  13. package/docs/images/agency_ui_identity_settings.png +0 -0
  14. package/docs/images/agency_ui_import_skills.jpeg +0 -0
  15. package/docs/images/agency_ui_knowledge.png +0 -0
  16. package/docs/images/agency_ui_mission_control.png +0 -0
  17. package/docs/images/agency_ui_skills_marketplace.png +0 -0
  18. package/docs/images/agent_ui_agent_config.png +0 -0
  19. package/package.json +1 -1
  20. package/src/api/db/migrations/004_nullable_human_refs.ts +129 -0
  21. package/src/api/db/migrations/005_agent_skills.ts +14 -0
  22. package/src/api/db/migrations/006_runtime_machine.ts +24 -0
  23. package/src/api/db/seed.ts +62 -46
  24. package/src/api/index.ts +11 -4
  25. package/src/api/lib/deploy.ts +412 -0
  26. package/src/api/lib/env-vars.ts +19 -0
  27. package/src/api/lib/exec.ts +77 -0
  28. package/src/api/lib/fleet-sync.ts +49 -32
  29. package/src/api/lib/fs-store.ts +350 -0
  30. package/src/api/lib/import-skills.ts +105 -0
  31. package/src/api/lib/metrics.ts +183 -0
  32. package/src/api/lib/processes.ts +82 -12
  33. package/src/api/lib/provision-openclaw.ts +407 -0
  34. package/src/api/lib/remote-deploy.ts +77 -0
  35. package/src/api/lib/ssh.ts +97 -0
  36. package/src/api/lib/sync-skills.ts +171 -0
  37. package/src/api/lib/tunnels.ts +7 -38
  38. package/src/api/routes/agents.ts +184 -132
  39. package/src/api/routes/documents.ts +24 -5
  40. package/src/api/routes/knowledge.ts +7 -5
  41. package/src/api/routes/machines.ts +107 -0
  42. package/src/api/routes/messages.ts +23 -19
  43. package/src/api/routes/repos.ts +74 -0
  44. package/src/api/routes/role-configs.ts +29 -46
  45. package/src/api/routes/skills.ts +198 -40
  46. package/src/api/routes/tasks.ts +24 -11
  47. package/src/cli/commands/init.ts +47 -18
  48. package/src/cli/commands/machines.ts +97 -0
  49. package/src/cli/commands/ps.ts +6 -4
  50. package/src/cli/commands/repos.ts +78 -0
  51. package/src/cli/commands/ssh.ts +14 -36
  52. package/src/cli/index.ts +5 -1
  53. package/src/daemon.ts +120 -1
  54. package/src/templates/solo/agents-config.md +39 -0
  55. package/src/templates/solo/agents.md +41 -0
  56. package/src/templates/solo/heartbeat.md +48 -0
  57. package/src/templates/solo/tools.md +35 -0
  58. package/dashboard/out/_next/static/chunks/app/layout-056f12675e691d12.js +0 -1
  59. package/dashboard/out/_next/static/chunks/app/page-80f01fdbb09b43c8.js +0 -1
  60. package/dashboard/out/_next/static/css/27d1ea794f04e96a.css +0 -1
  61. /package/dashboard/out/_next/static/{BRrkKiSxqTmex5oLQlOY5 → BIIuuS2pf7AQlPcSE2A6K}/_buildManifest.js +0 -0
  62. /package/dashboard/out/_next/static/{BRrkKiSxqTmex5oLQlOY5 → BIIuuS2pf7AQlPcSE2A6K}/_ssgManifest.js +0 -0
@@ -24,31 +24,35 @@ messages.get("/:taskId/messages", async (c) => {
24
24
 
25
25
  messages.post("/:taskId/messages", async (c) => {
26
26
  const taskId = c.req.param("taskId");
27
- const { from, content } = await c.req.json<{ from: string; content: string }>();
28
- const agent = await resolveAgent(from);
29
- if (!agent) return c.json({ error: "unknown agent" }, 400);
27
+ const { from, content } = await c.req.json<{ from?: string; content: string }>();
28
+ // "human" or missing from = NULL (human user)
29
+ const agent = from && from !== "human" ? await resolveAgent(from) : null;
30
+ if (from && from !== "human" && !agent) return c.json({ error: "unknown agent" }, 400);
30
31
 
31
32
  const msg = await db.insertInto("messages").values({
32
33
  id: crypto.randomUUID(),
33
- task_id: taskId, from_agent: agent.id, content,
34
+ task_id: taskId, from_agent: agent?.id ?? null, content,
34
35
  }).returningAll().executeTakeFirstOrThrow();
35
36
 
36
- // Collect notification targets
37
- const mentionIds = await parseMentions(content, agent.id);
38
- const subscriberIds = await getTaskSubscribers(taskId, agent.id);
39
- const targetIds = new Set([...mentionIds, ...subscriberIds]);
40
-
41
- // Create notifications
42
- for (const targetId of targetIds) {
43
- await db.insertInto("notifications").values({
44
- id: crypto.randomUUID(),
45
- target_agent: targetId,
46
- source_agent: agent.id,
47
- task_id: taskId,
48
- content: `${from}: ${content}`,
49
- }).execute();
37
+ // Collect notification targets (only if from an agent)
38
+ if (agent) {
39
+ const mentionIds = await parseMentions(content, agent.id);
40
+ const subscriberIds = await getTaskSubscribers(taskId, agent.id);
41
+ const targetIds = new Set([...mentionIds, ...subscriberIds]);
42
+
43
+ // Create notifications
44
+ for (const targetId of targetIds) {
45
+ await db.insertInto("notifications").values({
46
+ id: crypto.randomUUID(),
47
+ target_agent: targetId,
48
+ source_agent: agent.id,
49
+ task_id: taskId,
50
+ content: `${from}: ${content}`,
51
+ }).execute();
52
+ }
53
+
54
+ await logActivity("message", agent.id, `Message on task`, taskId);
50
55
  }
51
56
 
52
- await logActivity("message", agent.id, `Message on task`, taskId);
53
57
  return c.json(msg, 201);
54
58
  });
@@ -0,0 +1,74 @@
1
+ import { Hono } from "hono";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+
5
+ export interface Repo {
6
+ name: string;
7
+ url: string;
8
+ default_branch: string;
9
+ }
10
+
11
+ function resolveReposPath(): string {
12
+ return process.env.REPOS_PATH ?? path.resolve(process.cwd(), ".agency", "repos.json");
13
+ }
14
+
15
+ export function readRepos(): Repo[] {
16
+ const p = resolveReposPath();
17
+ if (!fs.existsSync(p)) return [];
18
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
19
+ }
20
+
21
+ function writeRepos(repos: Repo[]): void {
22
+ const p = resolveReposPath();
23
+ fs.mkdirSync(path.dirname(p), { recursive: true });
24
+ fs.writeFileSync(p, JSON.stringify(repos, null, 2) + "\n");
25
+ }
26
+
27
+ export const repos = new Hono();
28
+
29
+ repos.get("/", (c) => {
30
+ return c.json(readRepos());
31
+ });
32
+
33
+ repos.post("/", async (c) => {
34
+ const body = await c.req.json<{ name: string; url: string; default_branch?: string }>();
35
+ if (!body.name || !body.url) {
36
+ return c.json({ error: "name and url are required" }, 400);
37
+ }
38
+
39
+ const list = readRepos();
40
+ if (list.find((r) => r.name === body.name)) {
41
+ return c.json({ error: "repo already exists" }, 409);
42
+ }
43
+
44
+ list.push({ name: body.name, url: body.url, default_branch: body.default_branch || "main" });
45
+ writeRepos(list);
46
+ return c.json({ ok: true }, 201);
47
+ });
48
+
49
+ repos.put("/:name", async (c) => {
50
+ const name = c.req.param("name");
51
+ const list = readRepos();
52
+ const idx = list.findIndex((r) => r.name === name);
53
+ if (idx === -1) return c.json({ error: "not found" }, 404);
54
+
55
+ const body = await c.req.json<Partial<Repo>>();
56
+ list[idx] = {
57
+ name: list[idx].name,
58
+ url: body.url ?? list[idx].url,
59
+ default_branch: body.default_branch ?? list[idx].default_branch,
60
+ };
61
+ writeRepos(list);
62
+ return c.json(list[idx]);
63
+ });
64
+
65
+ repos.delete("/:name", (c) => {
66
+ const name = c.req.param("name");
67
+ const list = readRepos();
68
+ const filtered = list.filter((r) => r.name !== name);
69
+ if (filtered.length === list.length) {
70
+ return c.json({ error: "not found" }, 404);
71
+ }
72
+ writeRepos(filtered);
73
+ return c.json({ ok: true });
74
+ });
@@ -1,27 +1,31 @@
1
1
  import { Hono } from "hono";
2
- import { db } from "../db/client.js";
2
+ import {
3
+ listRoleConfigs,
4
+ getRoleConfig,
5
+ putRoleConfig,
6
+ deleteRoleConfig,
7
+ deleteRole,
8
+ } from "../lib/fs-store.js";
9
+ import { pushRoleToAllAgents } from "../lib/sync-skills.js";
3
10
 
4
11
  export const roleConfigs = new Hono();
5
12
 
6
13
  // List all role configs, optional ?role=
7
14
  roleConfigs.get("/", async (c) => {
8
- const role = c.req.query("role");
9
- let q = db.selectFrom("role_configs").selectAll();
10
- if (role) q = q.where("role", "=", role);
11
- const rows = await q.orderBy("role").orderBy("config_type").execute();
15
+ const role = c.req.query("role") || undefined;
16
+ const rows = listRoleConfigs(role);
12
17
  return c.json(rows);
13
18
  });
14
19
 
15
20
  // Get one role config by role + config_type
16
21
  roleConfigs.get("/:role/:configType", async (c) => {
17
- const row = await db
18
- .selectFrom("role_configs")
19
- .where("role", "=", c.req.param("role"))
20
- .where("config_type", "=", c.req.param("configType"))
21
- .selectAll()
22
- .executeTakeFirst();
23
- if (!row) return c.json({ error: "not found" }, 404);
24
- return c.json(row);
22
+ const content = getRoleConfig(c.req.param("role"), c.req.param("configType"));
23
+ if (content === null) return c.json({ error: "not found" }, 404);
24
+ return c.json({
25
+ role: c.req.param("role"),
26
+ config_type: c.req.param("configType"),
27
+ content,
28
+ });
25
29
  });
26
30
 
27
31
  // Upsert a role config
@@ -30,42 +34,21 @@ roleConfigs.put("/:role/:configType", async (c) => {
30
34
  const configType = c.req.param("configType");
31
35
  const { content } = await c.req.json<{ content: string }>();
32
36
 
33
- const existing = await db
34
- .selectFrom("role_configs")
35
- .where("role", "=", role)
36
- .where("config_type", "=", configType)
37
- .selectAll()
38
- .executeTakeFirst();
39
-
40
- if (existing) {
41
- const updated = await db
42
- .updateTable("role_configs")
43
- .where("id", "=", existing.id)
44
- .set({ content, updated_at: new Date().toISOString() })
45
- .returningAll()
46
- .executeTakeFirstOrThrow();
47
- return c.json(updated);
48
- }
49
-
50
- const row = await db
51
- .insertInto("role_configs")
52
- .values({
53
- id: crypto.randomUUID(),
54
- role,
55
- config_type: configType,
56
- content,
57
- })
58
- .returningAll()
59
- .executeTakeFirstOrThrow();
60
- return c.json(row, 201);
37
+ putRoleConfig(role, configType, content);
38
+ pushRoleToAllAgents(role).catch(() => {});
39
+ return c.json({ role, config_type: configType, content });
61
40
  });
62
41
 
63
42
  // Delete a role config
64
43
  roleConfigs.delete("/:role/:configType", async (c) => {
65
- await db
66
- .deleteFrom("role_configs")
67
- .where("role", "=", c.req.param("role"))
68
- .where("config_type", "=", c.req.param("configType"))
69
- .execute();
44
+ deleteRoleConfig(c.req.param("role"), c.req.param("configType"));
45
+ return c.json({ ok: true });
46
+ });
47
+
48
+ // Delete an entire role
49
+ roleConfigs.delete("/:role", async (c) => {
50
+ const role = c.req.param("role");
51
+ const deleted = deleteRole(role);
52
+ if (!deleted) return c.json({ error: "role not found" }, 404);
70
53
  return c.json({ ok: true });
71
54
  });
@@ -1,33 +1,144 @@
1
1
  import { Hono } from "hono";
2
- import { db } from "../db/client.js";
2
+ import { listSkills, getSkill, putSkill, deleteSkill, listSkillFiles, getSkillFile, putSkillFile, deleteSkillFile } from "../lib/fs-store.js";
3
+ import { pushSkillsToAllAgents } from "../lib/sync-skills.js";
4
+ import { cloneRepo, scanSkills, cleanupRepo, copySkillDir } from "../lib/import-skills.js";
3
5
 
4
6
  export const skills = new Hono();
5
7
 
8
+ function withId(skill: { name: string; body: string; category: string; tags: string[] }) {
9
+ return { id: skill.name, ...skill };
10
+ }
11
+
6
12
  // List skills, optional ?category=, ?search=
7
13
  skills.get("/", async (c) => {
8
14
  const category = c.req.query("category");
9
- const search = c.req.query("search");
15
+ const search = c.req.query("search")?.toLowerCase();
10
16
 
11
- let q = db.selectFrom("skills").selectAll();
12
- if (category) q = q.where("category", "=", category);
17
+ let rows = listSkills();
18
+ if (category) rows = rows.filter((r) => r.category === category);
13
19
  if (search) {
14
- q = q.where((eb) =>
15
- eb.or([
16
- eb("name", "like", `%${search}%`),
17
- eb("body", "like", `%${search}%`),
18
- ])
20
+ rows = rows.filter(
21
+ (r) =>
22
+ r.name.toLowerCase().includes(search) ||
23
+ r.body.toLowerCase().includes(search)
19
24
  );
20
25
  }
21
26
 
22
- const rows = await q.orderBy("name").execute();
23
- return c.json(rows.map((r) => ({ ...r, tags: JSON.parse(r.tags) })));
27
+ return c.json(rows.map(withId));
28
+ });
29
+
30
+ const KNOWN_REPOS = [
31
+ "https://github.com/anthropics/skills",
32
+ "https://github.com/obra/superpowers",
33
+ "https://github.com/ComposioHQ/awesome-claude-skills",
34
+ ];
35
+
36
+ // In-memory cache for available skills (5 min TTL)
37
+ let availableCache: { skills: { name: string; description: string; category: string; repo: string }[]; ts: number } | null = null;
38
+ const CACHE_TTL = 5 * 60 * 1000;
39
+
40
+ // List all available skills from known repos (cached)
41
+ skills.get("/available", async (c) => {
42
+ const force = c.req.query("refresh") === "1";
43
+ if (!force && availableCache && Date.now() - availableCache.ts < CACHE_TTL) {
44
+ return c.json(availableCache.skills);
45
+ }
46
+
47
+ const all: typeof availableCache["skills"] = [];
48
+ await Promise.allSettled(
49
+ KNOWN_REPOS.map(async (url) => {
50
+ let repoDir: string | undefined;
51
+ try {
52
+ repoDir = await cloneRepo(url);
53
+ const found = await scanSkills(repoDir);
54
+ const repo = url.replace(/\.git$/, "").split("/").slice(-2).join("/");
55
+ for (const s of found) {
56
+ all.push({ name: s.name, description: s.description, category: s.category, repo });
57
+ }
58
+ } finally {
59
+ if (repoDir) cleanupRepo(repoDir);
60
+ }
61
+ })
62
+ );
63
+ all.sort((a, b) => a.name.localeCompare(b.name));
64
+ availableCache = { skills: all, ts: Date.now() };
65
+ return c.json(all);
66
+ });
67
+
68
+ // Preview skills available in a GitHub repo
69
+ skills.get("/import/preview", async (c) => {
70
+ const url = c.req.query("url");
71
+ if (!url) return c.json({ error: "url query param required" }, 400);
72
+
73
+ let repoDir: string | undefined;
74
+ try {
75
+ repoDir = await cloneRepo(url);
76
+ const available = await scanSkills(repoDir);
77
+ const repoName = url.replace(/\.git$/, "").split("/").slice(-2).join("/");
78
+ return c.json({ repo: repoName, skills: available.map(({ body: _, ...s }) => s) });
79
+ } catch (err: any) {
80
+ return c.json({ error: err.message }, 500);
81
+ } finally {
82
+ if (repoDir) cleanupRepo(repoDir);
83
+ }
84
+ });
85
+
86
+ // Import skills from a GitHub repo
87
+ skills.post("/import", async (c) => {
88
+ const { url, skills: requested, overwrite } = await c.req.json<{
89
+ url: string;
90
+ skills?: string[];
91
+ overwrite?: boolean;
92
+ }>();
93
+ if (!url) return c.json({ error: "url is required" }, 400);
94
+
95
+ let repoDir: string | undefined;
96
+ try {
97
+ repoDir = await cloneRepo(url);
98
+ let available = await scanSkills(repoDir);
99
+
100
+ if (requested?.length) {
101
+ const set = new Set(requested);
102
+ available = available.filter((s) => set.has(s.name));
103
+ }
104
+
105
+ const imported: string[] = [];
106
+ const skipped: string[] = [];
107
+ const errors: string[] = [];
108
+
109
+ for (const skill of available) {
110
+ try {
111
+ const existing = getSkill(skill.name);
112
+ if (existing && !overwrite) {
113
+ skipped.push(skill.name);
114
+ continue;
115
+ }
116
+ // Copy entire skill directory if sourcePath available, otherwise just SKILL.md
117
+ if (skill.sourcePath) {
118
+ copySkillDir(skill.sourcePath, skill.name);
119
+ } else {
120
+ putSkill(skill.name, skill.body, skill.category);
121
+ }
122
+ imported.push(skill.name);
123
+ } catch (err: any) {
124
+ errors.push(`${skill.name}: ${err.message}`);
125
+ }
126
+ }
127
+
128
+ pushSkillsToAllAgents().catch(() => {});
129
+ return c.json({ imported, skipped, errors });
130
+ } catch (err: any) {
131
+ return c.json({ error: err.message }, 500);
132
+ } finally {
133
+ if (repoDir) cleanupRepo(repoDir);
134
+ }
24
135
  });
25
136
 
26
- // Get one skill
27
- skills.get("/:id", async (c) => {
28
- const row = await db.selectFrom("skills").where("id", "=", c.req.param("id")).selectAll().executeTakeFirst();
29
- if (!row) return c.json({ error: "not found" }, 404);
30
- return c.json({ ...row, tags: JSON.parse(row.tags) });
137
+ // Get one skill by name
138
+ skills.get("/:name", async (c) => {
139
+ const skill = getSkill(c.req.param("name"));
140
+ if (!skill) return c.json({ error: "not found" }, 404);
141
+ return c.json(withId(skill));
31
142
  });
32
143
 
33
144
  // Create skill
@@ -36,41 +147,88 @@ skills.post("/", async (c) => {
36
147
  name: string; body: string; category?: string; tags?: string[];
37
148
  }>();
38
149
 
39
- const row = await db
40
- .insertInto("skills")
41
- .values({
42
- id: crypto.randomUUID(),
43
- name: body.name,
44
- body: body.body,
45
- category: body.category ?? "general",
46
- tags: JSON.stringify(body.tags ?? []),
47
- })
48
- .returningAll()
49
- .executeTakeFirstOrThrow();
150
+ putSkill(body.name, body.body, body.category ?? "general", body.tags ?? []);
151
+ pushSkillsToAllAgents().catch(() => {});
50
152
 
51
- return c.json({ ...row, tags: JSON.parse(row.tags) }, 201);
153
+ const skill = getSkill(body.name)!;
154
+ return c.json(withId(skill), 201);
52
155
  });
53
156
 
54
157
  // Update skill
55
- skills.put("/:id", async (c) => {
56
- const id = c.req.param("id");
158
+ skills.put("/:name", async (c) => {
159
+ const name = c.req.param("name");
160
+ const existing = getSkill(name);
161
+ if (!existing) return c.json({ error: "not found" }, 404);
162
+
57
163
  const body = await c.req.json<{
58
164
  name?: string; body?: string; category?: string; tags?: string[];
59
165
  }>();
60
166
 
61
- let q = db.updateTable("skills").where("id", "=", id);
62
- if (body.name !== undefined) q = q.set("name", body.name);
63
- if (body.body !== undefined) q = q.set("body", body.body);
64
- if (body.category !== undefined) q = q.set("category", body.category);
65
- if (body.tags !== undefined) q = q.set("tags", JSON.stringify(body.tags));
66
- q = q.set("updated_at", new Date().toISOString());
167
+ const newName = body.name ?? name;
168
+ const newBody = body.body ?? existing.body;
169
+ const newCategory = body.category ?? existing.category;
170
+ const newTags = body.tags ?? existing.tags;
171
+
172
+ // If renamed, delete old first
173
+ if (newName !== name) {
174
+ deleteSkill(name);
175
+ }
176
+
177
+ putSkill(newName, newBody, newCategory, newTags);
178
+ pushSkillsToAllAgents().catch(() => {});
67
179
 
68
- const row = await q.returningAll().executeTakeFirstOrThrow();
69
- return c.json({ ...row, tags: JSON.parse(row.tags) });
180
+ return c.json(withId(getSkill(newName)!));
70
181
  });
71
182
 
72
183
  // Delete skill
73
- skills.delete("/:id", async (c) => {
74
- await db.deleteFrom("skills").where("id", "=", c.req.param("id")).execute();
184
+ skills.delete("/:name", async (c) => {
185
+ deleteSkill(c.req.param("name"));
186
+ pushSkillsToAllAgents().catch(() => {});
187
+ return c.json({ ok: true });
188
+ });
189
+
190
+ // ── Skill Files ──
191
+
192
+ // List files in a skill directory
193
+ skills.get("/:name/files", async (c) => {
194
+ const files = listSkillFiles(c.req.param("name"));
195
+ if (!files) return c.json({ error: "skill not found" }, 404);
196
+ return c.json(files);
197
+ });
198
+
199
+ // Get a specific file from a skill
200
+ skills.get("/:name/files/*", async (c) => {
201
+ const name = c.req.param("name");
202
+ const filePath = c.req.path.split(`/skills/${name}/files/`)[1];
203
+ if (!filePath) return c.json({ error: "file path required" }, 400);
204
+
205
+ const content = getSkillFile(name, filePath);
206
+ if (content === null) return c.json({ error: "file not found" }, 404);
207
+ return c.json({ path: filePath, content });
208
+ });
209
+
210
+ // Create/update a file in a skill
211
+ skills.put("/:name/files/*", async (c) => {
212
+ const name = c.req.param("name");
213
+ const filePath = c.req.path.split(`/skills/${name}/files/`)[1];
214
+ if (!filePath) return c.json({ error: "file path required" }, 400);
215
+
216
+ const { content } = await c.req.json<{ content: string }>();
217
+ if (content === undefined) return c.json({ error: "content required" }, 400);
218
+
219
+ putSkillFile(name, filePath, content);
220
+ pushSkillsToAllAgents().catch(() => {});
221
+ return c.json({ ok: true, path: filePath });
222
+ });
223
+
224
+ // Delete a file from a skill
225
+ skills.delete("/:name/files/*", async (c) => {
226
+ const name = c.req.param("name");
227
+ const filePath = c.req.path.split(`/skills/${name}/files/`)[1];
228
+ if (!filePath) return c.json({ error: "file path required" }, 400);
229
+
230
+ const deleted = deleteSkillFile(name, filePath);
231
+ if (!deleted) return c.json({ error: "file not found or cannot be deleted" }, 404);
232
+ pushSkillsToAllAgents().catch(() => {});
75
233
  return c.json({ ok: true });
76
234
  });
@@ -60,18 +60,19 @@ tasks.get("/:id", async (c) => {
60
60
 
61
61
  tasks.post("/", async (c) => {
62
62
  const body = await c.req.json<{
63
- title: string; description: string; from: string;
63
+ title: string; description: string; from?: string;
64
64
  design?: string; acceptance?: string; priority?: number;
65
65
  task_type?: string; parent_id?: string; assign?: string;
66
66
  }>();
67
- const agent = await resolveAgent(body.from);
68
- if (!agent) return c.json({ error: "unknown agent" }, 400);
67
+ // "human" or missing from = NULL (human user)
68
+ const agent = body.from && body.from !== "human" ? await resolveAgent(body.from) : null;
69
+ if (body.from && body.from !== "human" && !agent) return c.json({ error: "unknown agent" }, 400);
69
70
 
70
71
  const task = await db.insertInto("tasks").values({
71
72
  id: crypto.randomUUID(),
72
73
  title: body.title,
73
74
  description: body.description,
74
- created_by: agent.id,
75
+ created_by: agent?.id ?? null,
75
76
  design: body.design ?? null,
76
77
  acceptance: body.acceptance ?? null,
77
78
  priority: body.priority ?? 2,
@@ -80,7 +81,9 @@ tasks.post("/", async (c) => {
80
81
  status: body.assign ? "assigned" : "inbox",
81
82
  }).returningAll().executeTakeFirstOrThrow();
82
83
 
83
- await logActivity("task_created", agent.id, `Created task: ${task.title}`, task.id);
84
+ if (agent) {
85
+ await logActivity("task_created", agent.id, `Created task: ${task.title}`, task.id);
86
+ }
84
87
 
85
88
  if (body.assign) {
86
89
  const assignee = await resolveAgent(body.assign);
@@ -96,10 +99,10 @@ tasks.patch("/:id", async (c) => {
96
99
  const id = c.req.param("id");
97
100
  const body = await c.req.json<{
98
101
  status?: string; priority?: number; description?: string;
99
- design?: string; acceptance?: string; title?: string; from: string;
102
+ design?: string; acceptance?: string; title?: string; from?: string;
100
103
  }>();
101
- const agent = await resolveAgent(body.from);
102
- if (!agent) return c.json({ error: "unknown agent" }, 400);
104
+ // "human" or missing from = NULL (human user)
105
+ const agent = body.from && body.from !== "human" ? await resolveAgent(body.from) : null;
103
106
 
104
107
  let q = db.updateTable("tasks").where("id", "=", id);
105
108
  if (body.status !== undefined) q = q.set("status", body.status);
@@ -112,7 +115,7 @@ tasks.patch("/:id", async (c) => {
112
115
 
113
116
  const updated = await q.returningAll().executeTakeFirstOrThrow();
114
117
 
115
- if (body.status) {
118
+ if (body.status && agent) {
116
119
  await logActivity("status_changed", agent.id, `Status → ${body.status}`, id);
117
120
  }
118
121
 
@@ -125,7 +128,18 @@ tasks.post("/:id/assign", async (c) => {
125
128
  const agent = await resolveAgent(agentName);
126
129
  if (!agent) return c.json({ error: "unknown agent" }, 400);
127
130
 
128
- await db.insertInto("task_assignees").values({ task_id: id, agent_id: agent.id }).execute();
131
+ // Check if already assigned
132
+ const existing = await db
133
+ .selectFrom("task_assignees")
134
+ .where("task_id", "=", id)
135
+ .where("agent_id", "=", agent.id)
136
+ .selectAll()
137
+ .executeTakeFirst();
138
+
139
+ if (!existing) {
140
+ await db.insertInto("task_assignees").values({ task_id: id, agent_id: agent.id }).execute();
141
+ await logActivity("assigned", agent.id, `Assigned to ${agentName}`, id);
142
+ }
129
143
 
130
144
  // inbox → assigned
131
145
  await db.updateTable("tasks")
@@ -135,7 +149,6 @@ tasks.post("/:id/assign", async (c) => {
135
149
  .set("updated_at", new Date().toISOString())
136
150
  .execute();
137
151
 
138
- await logActivity("assigned", agent.id, `Assigned to ${agentName}`, id);
139
152
  return c.json({ ok: true });
140
153
  });
141
154