bopodev-api 0.1.34 → 0.1.35

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 (90) hide show
  1. package/package.json +5 -5
  2. package/src/app.ts +4 -2
  3. package/src/assets/starter-packs/customer-support-excellence.zip +0 -0
  4. package/src/assets/starter-packs/devrel-growth.zip +0 -0
  5. package/src/assets/starter-packs/product-delivery-trio.zip +0 -0
  6. package/src/assets/starter-packs/revenue-gtm-b2b.zip +0 -0
  7. package/src/assets/starter-packs/sources/customer-support-excellence/.bopo.yaml +129 -0
  8. package/src/assets/starter-packs/sources/customer-support-excellence/COMPANY.md +7 -0
  9. package/src/assets/starter-packs/sources/customer-support-excellence/README.md +3 -0
  10. package/src/assets/starter-packs/sources/customer-support-excellence/agents/founder-ceo/HEARTBEAT.md +5 -0
  11. package/src/assets/starter-packs/sources/customer-support-excellence/agents/support-lead/HEARTBEAT.md +5 -0
  12. package/src/assets/starter-packs/sources/customer-support-excellence/agents/support-specialist/HEARTBEAT.md +5 -0
  13. package/src/assets/starter-packs/sources/customer-support-excellence/projects/knowledge-base/PROJECT.md +7 -0
  14. package/src/assets/starter-packs/sources/customer-support-excellence/projects/quality/PROJECT.md +7 -0
  15. package/src/assets/starter-packs/sources/customer-support-excellence/projects/queue/PROJECT.md +7 -0
  16. package/src/assets/starter-packs/sources/customer-support-excellence/skills/kb-article-skeleton/SKILL.md +17 -0
  17. package/src/assets/starter-packs/sources/customer-support-excellence/skills/ticket-response-playbook/SKILL.md +15 -0
  18. package/src/assets/starter-packs/sources/customer-support-excellence/tasks/daily-queue-standup/TASK.md +11 -0
  19. package/src/assets/starter-packs/sources/customer-support-excellence/tasks/kb-gap-sweep/TASK.md +11 -0
  20. package/src/assets/starter-packs/sources/devrel-growth/.bopo.yaml +128 -0
  21. package/src/assets/starter-packs/sources/devrel-growth/COMPANY.md +7 -0
  22. package/src/assets/starter-packs/sources/devrel-growth/README.md +3 -0
  23. package/src/assets/starter-packs/sources/devrel-growth/agents/content-producer/HEARTBEAT.md +5 -0
  24. package/src/assets/starter-packs/sources/devrel-growth/agents/devrel-lead/HEARTBEAT.md +5 -0
  25. package/src/assets/starter-packs/sources/devrel-growth/agents/founder-ceo/HEARTBEAT.md +5 -0
  26. package/src/assets/starter-packs/sources/devrel-growth/projects/community/PROJECT.md +7 -0
  27. package/src/assets/starter-packs/sources/devrel-growth/projects/docs-education/PROJECT.md +7 -0
  28. package/src/assets/starter-packs/sources/devrel-growth/projects/partners/PROJECT.md +7 -0
  29. package/src/assets/starter-packs/sources/devrel-growth/skills/changelog-to-post/SKILL.md +14 -0
  30. package/src/assets/starter-packs/sources/devrel-growth/skills/tutorial-outline/SKILL.md +15 -0
  31. package/src/assets/starter-packs/sources/devrel-growth/tasks/community-health-review/TASK.md +11 -0
  32. package/src/assets/starter-packs/sources/devrel-growth/tasks/weekly-content-plan/TASK.md +11 -0
  33. package/src/assets/starter-packs/sources/product-delivery-trio/.bopo.yaml +138 -0
  34. package/src/assets/starter-packs/sources/product-delivery-trio/COMPANY.md +7 -0
  35. package/src/assets/starter-packs/sources/product-delivery-trio/README.md +9 -0
  36. package/src/assets/starter-packs/sources/product-delivery-trio/agents/engineer-ic/HEARTBEAT.md +5 -0
  37. package/src/assets/starter-packs/sources/product-delivery-trio/agents/founder-ceo/HEARTBEAT.md +6 -0
  38. package/src/assets/starter-packs/sources/product-delivery-trio/agents/product-lead/HEARTBEAT.md +5 -0
  39. package/src/assets/starter-packs/sources/product-delivery-trio/projects/delivery/PROJECT.md +7 -0
  40. package/src/assets/starter-packs/sources/product-delivery-trio/projects/quality/PROJECT.md +7 -0
  41. package/src/assets/starter-packs/sources/product-delivery-trio/projects/strategy/PROJECT.md +7 -0
  42. package/src/assets/starter-packs/sources/product-delivery-trio/skills/issue-triage/SKILL.md +21 -0
  43. package/src/assets/starter-packs/sources/product-delivery-trio/skills/rca-template/SKILL.md +16 -0
  44. package/src/assets/starter-packs/sources/product-delivery-trio/tasks/release-hygiene/TASK.md +11 -0
  45. package/src/assets/starter-packs/sources/product-delivery-trio/tasks/weekly-leadership-sync/TASK.md +11 -0
  46. package/src/assets/starter-packs/sources/revenue-gtm-b2b/.bopo.yaml +132 -0
  47. package/src/assets/starter-packs/sources/revenue-gtm-b2b/COMPANY.md +7 -0
  48. package/src/assets/starter-packs/sources/revenue-gtm-b2b/README.md +3 -0
  49. package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/founder-ceo/HEARTBEAT.md +5 -0
  50. package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/gtm-lead/HEARTBEAT.md +5 -0
  51. package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/pipeline-owner/HEARTBEAT.md +5 -0
  52. package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/customer-success/PROJECT.md +7 -0
  53. package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/deals/PROJECT.md +7 -0
  54. package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/pipeline/PROJECT.md +7 -0
  55. package/src/assets/starter-packs/sources/revenue-gtm-b2b/skills/discovery-call-brief/SKILL.md +14 -0
  56. package/src/assets/starter-packs/sources/revenue-gtm-b2b/skills/icp-scoring/SKILL.md +20 -0
  57. package/src/assets/starter-packs/sources/revenue-gtm-b2b/tasks/pipeline-hygiene/TASK.md +11 -0
  58. package/src/assets/starter-packs/sources/revenue-gtm-b2b/tasks/weekly-revenue-review/TASK.md +11 -0
  59. package/src/lib/agent-issue-permissions.ts +56 -0
  60. package/src/lib/builtin-bopo-skills/bopodev-control-plane.md +7 -0
  61. package/src/realtime/office-space.ts +7 -0
  62. package/src/routes/agents.ts +23 -1
  63. package/src/routes/assistant.ts +40 -1
  64. package/src/routes/companies.ts +227 -15
  65. package/src/routes/issues.ts +48 -0
  66. package/src/routes/plugins.ts +393 -103
  67. package/src/routes/{loops.ts → routines.ts} +72 -76
  68. package/src/scripts/onboard-seed.ts +2 -0
  69. package/src/server.ts +3 -1
  70. package/src/services/company-assistant-context-snapshot.ts +4 -2
  71. package/src/services/company-assistant-service.ts +17 -15
  72. package/src/services/company-file-archive-service.ts +56 -3
  73. package/src/services/company-file-import-service.ts +210 -31
  74. package/src/services/governance-service.ts +58 -3
  75. package/src/services/heartbeat-service/heartbeat-run.ts +7 -0
  76. package/src/services/plugin-artifact-installer.ts +115 -0
  77. package/src/services/plugin-artifact-store.ts +28 -0
  78. package/src/services/plugin-capability-policy.ts +31 -0
  79. package/src/services/plugin-jobs-service.ts +74 -0
  80. package/src/services/plugin-manifest-loader.ts +78 -3
  81. package/src/services/plugin-rpc.ts +102 -0
  82. package/src/services/plugin-runtime.ts +240 -209
  83. package/src/services/plugin-worker-host.ts +167 -0
  84. package/src/services/starter-pack-registry.ts +68 -0
  85. package/src/services/template-apply-service.ts +3 -1
  86. package/src/services/template-catalog.ts +29 -0
  87. package/src/services/work-loop-service/work-loop-service.ts +18 -18
  88. package/src/shutdown/graceful-shutdown.ts +3 -1
  89. package/src/worker/scheduler.ts +21 -1
  90. package/src/services/company-export-service.ts +0 -63
@@ -1,20 +1,16 @@
1
1
  import { Router } from "express";
2
2
  import { appendAuditEvent, listAuditEvents } from "bopodev-db";
3
3
  import {
4
- WorkLoopCreateRequestSchema,
5
- WorkLoopTriggerCreateRequestSchema,
6
- WorkLoopUpdateRequestSchema,
7
- WorkLoopTriggerUpdateRequestSchema
4
+ WorkRoutineCreateRequestSchema,
5
+ WorkRoutineTriggerCreateRequestSchema,
6
+ WorkRoutineUpdateRequestSchema,
7
+ WorkRoutineTriggerUpdateRequestSchema
8
8
  } from "bopodev-contracts";
9
9
  import type { AppContext } from "../context";
10
10
  import { sendError, sendOk } from "../http";
11
11
  import { requireCompanyScope } from "../middleware/company-scope";
12
12
  import { enforcePermission } from "../middleware/request-actor";
13
- import {
14
- workLoopRuns,
15
- workLoops,
16
- workLoopTriggers
17
- } from "bopodev-db";
13
+ import { workLoopRuns, workLoops, workLoopTriggers } from "bopodev-db";
18
14
  import {
19
15
  addWorkLoopTrigger,
20
16
  addWorkLoopTriggerFromPreset,
@@ -29,7 +25,7 @@ import {
29
25
  deleteWorkLoopTrigger
30
26
  } from "../services/work-loop-service";
31
27
 
32
- function serializeLoop(row: typeof workLoops.$inferSelect) {
28
+ function serializeRoutine(row: typeof workLoops.$inferSelect) {
33
29
  let goalIds: string[] = [];
34
30
  try {
35
31
  goalIds = JSON.parse(row.goalIdsJson || "[]") as string[];
@@ -59,7 +55,7 @@ function serializeTrigger(row: typeof workLoopTriggers.$inferSelect) {
59
55
  return {
60
56
  id: row.id,
61
57
  companyId: row.companyId,
62
- workLoopId: row.workLoopId,
58
+ routineId: row.routineId,
63
59
  kind: row.kind,
64
60
  label: row.label,
65
61
  enabled: row.enabled,
@@ -77,7 +73,7 @@ function serializeRun(row: typeof workLoopRuns.$inferSelect) {
77
73
  return {
78
74
  id: row.id,
79
75
  companyId: row.companyId,
80
- workLoopId: row.workLoopId,
76
+ routineId: row.routineId,
81
77
  triggerId: row.triggerId,
82
78
  source: row.source,
83
79
  status: row.status,
@@ -92,23 +88,23 @@ function serializeRun(row: typeof workLoopRuns.$inferSelect) {
92
88
  };
93
89
  }
94
90
 
95
- export function createLoopsRouter(ctx: AppContext) {
91
+ export function createRoutinesRouter(ctx: AppContext) {
96
92
  const router = Router();
97
93
  router.use(requireCompanyScope);
98
94
 
99
95
  router.get("/", async (req, res) => {
100
- if (!enforcePermission(req, res, "loops:read")) {
96
+ if (!enforcePermission(req, res, "routines:read")) {
101
97
  return;
102
98
  }
103
99
  const rows = await listWorkLoops(ctx.db, req.companyId!);
104
- return sendOk(res, { data: rows.map(serializeLoop) });
100
+ return sendOk(res, { data: rows.map(serializeRoutine) });
105
101
  });
106
102
 
107
103
  router.post("/", async (req, res) => {
108
- if (!enforcePermission(req, res, "loops:write")) {
104
+ if (!enforcePermission(req, res, "routines:write")) {
109
105
  return;
110
106
  }
111
- const parsed = WorkLoopCreateRequestSchema.safeParse(req.body);
107
+ const parsed = WorkRoutineCreateRequestSchema.safeParse(req.body);
112
108
  if (!parsed.success) {
113
109
  return sendError(res, parsed.error.message, 422);
114
110
  }
@@ -127,7 +123,7 @@ export function createLoopsRouter(ctx: AppContext) {
127
123
  catchUpPolicy: parsed.data.catchUpPolicy
128
124
  });
129
125
  if (!row) {
130
- return sendError(res, "Failed to create work loop.", 500);
126
+ return sendError(res, "Failed to create routine.", 500);
131
127
  }
132
128
  await appendAuditEvent(ctx.db, {
133
129
  companyId: req.companyId!,
@@ -137,47 +133,47 @@ export function createLoopsRouter(ctx: AppContext) {
137
133
  entityType: "work_loop",
138
134
  entityId: row.id,
139
135
  correlationId: req.requestId ?? null,
140
- payload: { loopId: row.id, title: row.title }
136
+ payload: { routineId: row.id, title: row.title }
141
137
  });
142
- return sendOk(res, { data: serializeLoop(row) });
138
+ return sendOk(res, { data: serializeRoutine(row) });
143
139
  } catch (e) {
144
- return sendError(res, e instanceof Error ? e.message : "Failed to create work loop.", 422);
140
+ return sendError(res, e instanceof Error ? e.message : "Failed to create routine.", 422);
145
141
  }
146
142
  });
147
143
 
148
- router.get("/:loopId", async (req, res) => {
149
- if (!enforcePermission(req, res, "loops:read")) {
144
+ router.get("/:routineId", async (req, res) => {
145
+ if (!enforcePermission(req, res, "routines:read")) {
150
146
  return;
151
147
  }
152
- const loopId = req.params.loopId;
153
- const row = await getWorkLoop(ctx.db, req.companyId!, loopId);
148
+ const routineId = req.params.routineId;
149
+ const row = await getWorkLoop(ctx.db, req.companyId!, routineId);
154
150
  if (!row) {
155
- return sendError(res, "Work loop not found.", 404);
151
+ return sendError(res, "Routine not found.", 404);
156
152
  }
157
153
  const [triggers, recentRuns] = await Promise.all([
158
- listWorkLoopTriggers(ctx.db, req.companyId!, loopId),
159
- listWorkLoopRuns(ctx.db, req.companyId!, loopId, 30)
154
+ listWorkLoopTriggers(ctx.db, req.companyId!, routineId),
155
+ listWorkLoopRuns(ctx.db, req.companyId!, routineId, 30)
160
156
  ]);
161
157
  return sendOk(res, {
162
158
  data: {
163
- ...serializeLoop(row),
159
+ ...serializeRoutine(row),
164
160
  triggers: triggers.map(serializeTrigger),
165
161
  recentRuns: recentRuns.map(serializeRun)
166
162
  }
167
163
  });
168
164
  });
169
165
 
170
- router.patch("/:loopId", async (req, res) => {
171
- if (!enforcePermission(req, res, "loops:write")) {
166
+ router.patch("/:routineId", async (req, res) => {
167
+ if (!enforcePermission(req, res, "routines:write")) {
172
168
  return;
173
169
  }
174
- const parsed = WorkLoopUpdateRequestSchema.safeParse(req.body);
170
+ const parsed = WorkRoutineUpdateRequestSchema.safeParse(req.body);
175
171
  if (!parsed.success) {
176
172
  return sendError(res, parsed.error.message, 422);
177
173
  }
178
- const row = await updateWorkLoop(ctx.db, req.companyId!, req.params.loopId, parsed.data);
174
+ const row = await updateWorkLoop(ctx.db, req.companyId!, req.params.routineId, parsed.data);
179
175
  if (!row) {
180
- return sendError(res, "Work loop not found.", 404);
176
+ return sendError(res, "Routine not found.", 404);
181
177
  }
182
178
  await appendAuditEvent(ctx.db, {
183
179
  companyId: req.companyId!,
@@ -189,29 +185,29 @@ export function createLoopsRouter(ctx: AppContext) {
189
185
  correlationId: req.requestId ?? null,
190
186
  payload: { patch: parsed.data }
191
187
  });
192
- return sendOk(res, { data: serializeLoop(row) });
188
+ return sendOk(res, { data: serializeRoutine(row) });
193
189
  });
194
190
 
195
- router.post("/:loopId/run", async (req, res) => {
196
- if (!enforcePermission(req, res, "loops:run")) {
191
+ router.post("/:routineId/run", async (req, res) => {
192
+ if (!enforcePermission(req, res, "routines:run")) {
197
193
  return;
198
194
  }
199
- const loopId = req.params.loopId;
200
- const loop = await getWorkLoop(ctx.db, req.companyId!, loopId);
195
+ const routineId = req.params.routineId;
196
+ const loop = await getWorkLoop(ctx.db, req.companyId!, routineId);
201
197
  if (!loop) {
202
- return sendError(res, "Work loop not found.", 404);
198
+ return sendError(res, "Routine not found.", 404);
203
199
  }
204
200
  const run = await dispatchLoopRun(ctx.db, {
205
201
  companyId: req.companyId!,
206
- loopId,
202
+ loopId: routineId,
207
203
  triggerId: null,
208
204
  source: "manual",
209
- idempotencyKey: req.requestId ? `manual:${loopId}:${req.requestId}` : `manual:${loopId}:${Date.now()}`,
205
+ idempotencyKey: req.requestId ? `manual:${routineId}:${req.requestId}` : `manual:${routineId}:${Date.now()}`,
210
206
  realtimeHub: ctx.realtimeHub,
211
207
  requestId: req.requestId
212
208
  });
213
209
  if (!run) {
214
- return sendError(res, "Work loop is not active or could not be dispatched.", 409);
210
+ return sendError(res, "Routine is not active or could not be dispatched.", 409);
215
211
  }
216
212
  await appendAuditEvent(ctx.db, {
217
213
  companyId: req.companyId!,
@@ -219,37 +215,37 @@ export function createLoopsRouter(ctx: AppContext) {
219
215
  actorId: req.actor?.id ?? null,
220
216
  eventType: "work_loop.manual_run",
221
217
  entityType: "work_loop",
222
- entityId: loopId,
218
+ entityId: routineId,
223
219
  correlationId: req.requestId ?? null,
224
220
  payload: { runId: run.id, status: run.status }
225
221
  });
226
222
  return sendOk(res, { data: serializeRun(run) });
227
223
  });
228
224
 
229
- router.get("/:loopId/runs", async (req, res) => {
230
- if (!enforcePermission(req, res, "loops:read")) {
225
+ router.get("/:routineId/runs", async (req, res) => {
226
+ if (!enforcePermission(req, res, "routines:read")) {
231
227
  return;
232
228
  }
233
- const loop = await getWorkLoop(ctx.db, req.companyId!, req.params.loopId);
229
+ const loop = await getWorkLoop(ctx.db, req.companyId!, req.params.routineId);
234
230
  if (!loop) {
235
- return sendError(res, "Work loop not found.", 404);
231
+ return sendError(res, "Routine not found.", 404);
236
232
  }
237
233
  const limit = Math.min(500, Math.max(1, Number(req.query.limit) || 100));
238
- const runs = await listWorkLoopRuns(ctx.db, req.companyId!, req.params.loopId, limit);
234
+ const runs = await listWorkLoopRuns(ctx.db, req.companyId!, req.params.routineId, limit);
239
235
  return sendOk(res, { data: runs.map(serializeRun) });
240
236
  });
241
237
 
242
- router.get("/:loopId/activity", async (req, res) => {
243
- if (!enforcePermission(req, res, "loops:read")) {
238
+ router.get("/:routineId/activity", async (req, res) => {
239
+ if (!enforcePermission(req, res, "routines:read")) {
244
240
  return;
245
241
  }
246
- const loopId = req.params.loopId;
247
- const loop = await getWorkLoop(ctx.db, req.companyId!, loopId);
242
+ const routineId = req.params.routineId;
243
+ const loop = await getWorkLoop(ctx.db, req.companyId!, routineId);
248
244
  if (!loop) {
249
- return sendError(res, "Work loop not found.", 404);
245
+ return sendError(res, "Routine not found.", 404);
250
246
  }
251
247
  const events = await listAuditEvents(ctx.db, req.companyId!, 200);
252
- const filtered = events.filter((e) => e.entityType === "work_loop" && e.entityId === loopId);
248
+ const filtered = events.filter((e) => e.entityType === "work_loop" && e.entityId === routineId);
253
249
  return sendOk(res, {
254
250
  data: filtered.map((e) => ({
255
251
  id: e.id,
@@ -262,16 +258,16 @@ export function createLoopsRouter(ctx: AppContext) {
262
258
  });
263
259
  });
264
260
 
265
- router.post("/:loopId/triggers", async (req, res) => {
266
- if (!enforcePermission(req, res, "loops:write")) {
261
+ router.post("/:routineId/triggers", async (req, res) => {
262
+ if (!enforcePermission(req, res, "routines:write")) {
267
263
  return;
268
264
  }
269
- const loopId = req.params.loopId;
270
- const loop = await getWorkLoop(ctx.db, req.companyId!, loopId);
265
+ const routineId = req.params.routineId;
266
+ const loop = await getWorkLoop(ctx.db, req.companyId!, routineId);
271
267
  if (!loop) {
272
- return sendError(res, "Work loop not found.", 404);
268
+ return sendError(res, "Routine not found.", 404);
273
269
  }
274
- const parsed = WorkLoopTriggerCreateRequestSchema.safeParse(req.body);
270
+ const parsed = WorkRoutineTriggerCreateRequestSchema.safeParse(req.body);
275
271
  if (!parsed.success) {
276
272
  return sendError(res, parsed.error.message, 422);
277
273
  }
@@ -281,7 +277,7 @@ export function createLoopsRouter(ctx: AppContext) {
281
277
  body.mode === "cron"
282
278
  ? await addWorkLoopTrigger(ctx.db, {
283
279
  companyId: req.companyId!,
284
- workLoopId: loopId,
280
+ routineId,
285
281
  cronExpression: body.cronExpression,
286
282
  timezone: body.timezone,
287
283
  label: body.label ?? null,
@@ -289,7 +285,7 @@ export function createLoopsRouter(ctx: AppContext) {
289
285
  })
290
286
  : await addWorkLoopTriggerFromPreset(ctx.db, {
291
287
  companyId: req.companyId!,
292
- workLoopId: loopId,
288
+ routineId,
293
289
  preset: body.preset,
294
290
  hour24: body.hour24,
295
291
  minute: body.minute,
@@ -307,21 +303,21 @@ export function createLoopsRouter(ctx: AppContext) {
307
303
  }
308
304
  });
309
305
 
310
- router.patch("/:loopId/triggers/:triggerId", async (req, res) => {
311
- if (!enforcePermission(req, res, "loops:write")) {
306
+ router.patch("/:routineId/triggers/:triggerId", async (req, res) => {
307
+ if (!enforcePermission(req, res, "routines:write")) {
312
308
  return;
313
309
  }
314
- const parsed = WorkLoopTriggerUpdateRequestSchema.safeParse(req.body);
310
+ const parsed = WorkRoutineTriggerUpdateRequestSchema.safeParse(req.body);
315
311
  if (!parsed.success) {
316
312
  return sendError(res, parsed.error.message, 422);
317
313
  }
318
- const loop = await getWorkLoop(ctx.db, req.companyId!, req.params.loopId);
314
+ const loop = await getWorkLoop(ctx.db, req.companyId!, req.params.routineId);
319
315
  if (!loop) {
320
- return sendError(res, "Work loop not found.", 404);
316
+ return sendError(res, "Routine not found.", 404);
321
317
  }
322
318
  try {
323
319
  const row = await updateWorkLoopTrigger(ctx.db, req.companyId!, req.params.triggerId, parsed.data);
324
- if (!row || row.workLoopId !== req.params.loopId) {
320
+ if (!row || row.routineId !== req.params.routineId) {
325
321
  return sendError(res, "Trigger not found.", 404);
326
322
  }
327
323
  return sendOk(res, { data: serializeTrigger(row) });
@@ -330,16 +326,16 @@ export function createLoopsRouter(ctx: AppContext) {
330
326
  }
331
327
  });
332
328
 
333
- router.delete("/:loopId/triggers/:triggerId", async (req, res) => {
334
- if (!enforcePermission(req, res, "loops:write")) {
329
+ router.delete("/:routineId/triggers/:triggerId", async (req, res) => {
330
+ if (!enforcePermission(req, res, "routines:write")) {
335
331
  return;
336
332
  }
337
- const { loopId, triggerId } = req.params;
338
- const loop = await getWorkLoop(ctx.db, req.companyId!, loopId);
333
+ const { routineId, triggerId } = req.params;
334
+ const loop = await getWorkLoop(ctx.db, req.companyId!, routineId);
339
335
  if (!loop) {
340
- return sendError(res, "Work loop not found.", 404);
336
+ return sendError(res, "Routine not found.", 404);
341
337
  }
342
- const deleted = await deleteWorkLoopTrigger(ctx.db, req.companyId!, loopId, triggerId);
338
+ const deleted = await deleteWorkLoopTrigger(ctx.db, req.companyId!, routineId, triggerId);
343
339
  if (!deleted) {
344
340
  return sendError(res, "Trigger not found.", 404);
345
341
  }
@@ -349,7 +345,7 @@ export function createLoopsRouter(ctx: AppContext) {
349
345
  actorId: req.actor?.id ?? null,
350
346
  eventType: "work_loop.trigger_deleted",
351
347
  entityType: "work_loop",
352
- entityId: loopId,
348
+ entityId: routineId,
353
349
  correlationId: req.requestId ?? null,
354
350
  payload: { triggerId }
355
351
  });
@@ -146,6 +146,8 @@ export async function ensureOnboardingSeed(input: {
146
146
  heartbeatCron: "*/5 * * * *",
147
147
  monthlyBudgetUsd: "100.0000",
148
148
  canHireAgents: true,
149
+ canAssignAgents: true,
150
+ canCreateIssues: true,
149
151
  ...runtimeConfigToDb(ceoCreateRuntimeConfig),
150
152
  initialState: runtimeConfigToStateBlobPatch(ceoCreateRuntimeConfig)
151
153
  });
package/src/server.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  resolvePublicBaseUrl
14
14
  } from "./security/deployment-mode";
15
15
  import { ensureBuiltinPluginsRegistered } from "./services/plugin-runtime";
16
+ import { pluginWorkerHost } from "./services/plugin-worker-host";
16
17
  import { ensureBuiltinTemplatesRegistered } from "./services/template-catalog";
17
18
  import { createHeartbeatScheduler } from "./worker/scheduler";
18
19
  import { bootstrapDatabaseWithStartupLogging } from "./startup/database";
@@ -102,7 +103,8 @@ async function main() {
102
103
  server,
103
104
  realtimeHub,
104
105
  dbClient,
105
- scheduler
106
+ scheduler,
107
+ pluginWorkers: pluginWorkerHost
106
108
  });
107
109
  }
108
110
 
@@ -33,7 +33,7 @@ function serializeIssue(row: Record<string, unknown>, goalIds: string[]) {
33
33
  id: row.id,
34
34
  projectId: row.projectId,
35
35
  parentIssueId: row.parentIssueId ?? null,
36
- loopId: row.loopId ?? null,
36
+ routineId: row.routineId ?? null,
37
37
  title: row.title,
38
38
  body: row.body ?? null,
39
39
  status: row.status,
@@ -104,7 +104,9 @@ function sanitizeAgentRow(row: Record<string, unknown>) {
104
104
  managerAgentId: row.managerAgentId ?? null,
105
105
  providerType: row.providerType,
106
106
  heartbeatCron: row.heartbeatCron,
107
- canHireAgents: row.canHireAgents ?? null
107
+ canHireAgents: row.canHireAgents ?? null,
108
+ canAssignAgents: row.canAssignAgents ?? null,
109
+ canCreateIssues: row.canCreateIssues ?? null
108
110
  };
109
111
  }
110
112
 
@@ -186,7 +186,7 @@ function serializeIssue(row: Record<string, unknown>, goalIds: string[]) {
186
186
  id: row.id,
187
187
  projectId: row.projectId,
188
188
  parentIssueId: row.parentIssueId ?? null,
189
- loopId: row.loopId ?? null,
189
+ routineId: row.routineId ?? null,
190
190
  title: row.title,
191
191
  body: row.body ?? null,
192
192
  status: row.status,
@@ -200,7 +200,7 @@ function serializeIssue(row: Record<string, unknown>, goalIds: string[]) {
200
200
  };
201
201
  }
202
202
 
203
- function serializeWorkLoopRow(row: Record<string, unknown>) {
203
+ function serializeRoutineRow(row: Record<string, unknown>) {
204
204
  return {
205
205
  id: row.id,
206
206
  projectId: row.projectId,
@@ -226,7 +226,9 @@ function sanitizeAgentRow(row: Record<string, unknown>) {
226
226
  managerAgentId: row.managerAgentId ?? null,
227
227
  providerType: row.providerType,
228
228
  heartbeatCron: row.heartbeatCron,
229
- canHireAgents: row.canHireAgents ?? null
229
+ canHireAgents: row.canHireAgents ?? null,
230
+ canAssignAgents: row.canAssignAgents ?? null,
231
+ canCreateIssues: row.canCreateIssues ?? null
230
232
  };
231
233
  }
232
234
 
@@ -303,17 +305,17 @@ export const ASSISTANT_TOOLS: AssistantToolDefinition[] = [
303
305
  }
304
306
  },
305
307
  {
306
- name: "list_work_loops",
307
- description: "List recurring work loops.",
308
+ name: "list_routines",
309
+ description: "List recurring routines (scheduled work that opens issues per run).",
308
310
  inputSchema: { type: "object", properties: {}, additionalProperties: false }
309
311
  },
310
312
  {
311
- name: "get_work_loop",
312
- description: "Get one work loop by id.",
313
+ name: "get_routine",
314
+ description: "Get one routine by id.",
313
315
  inputSchema: {
314
316
  type: "object",
315
- properties: { loop_id: { type: "string" } },
316
- required: ["loop_id"],
317
+ properties: { routine_id: { type: "string" } },
318
+ required: ["routine_id"],
317
319
  additionalProperties: false
318
320
  }
319
321
  },
@@ -595,14 +597,14 @@ export async function executeAssistantTool(
595
597
  const g = goals.find((x) => x.id === goalId);
596
598
  return capToolOutput(g ?? { error: "goal_not_found" });
597
599
  }
598
- case "list_work_loops": {
600
+ case "list_routines": {
599
601
  const loops = await listWorkLoops(db, companyId);
600
- return capToolOutput(loops.map((l) => serializeWorkLoopRow(l as unknown as Record<string, unknown>)));
602
+ return capToolOutput(loops.map((l) => serializeRoutineRow(l as unknown as Record<string, unknown>)));
601
603
  }
602
- case "get_work_loop": {
603
- const loopId = String(args.loop_id ?? "").trim();
604
- const row = await getWorkLoop(db, companyId, loopId);
605
- return capToolOutput(row ? serializeWorkLoopRow(row as unknown as Record<string, unknown>) : { error: "loop_not_found" });
604
+ case "get_routine": {
605
+ const routineId = String(args.routine_id ?? "").trim();
606
+ const row = await getWorkLoop(db, companyId, routineId);
607
+ return capToolOutput(row ? serializeRoutineRow(row as unknown as Record<string, unknown>) : { error: "routine_not_found" });
606
608
  }
607
609
  case "list_agents": {
608
610
  const agents = await listAgents(db, companyId);
@@ -4,7 +4,7 @@ import type { Readable } from "node:stream";
4
4
  import archiver from "archiver";
5
5
  import { stringify as yamlStringify } from "yaml";
6
6
  import type { BopoDb } from "bopodev-db";
7
- import { getCompany, listAgents, listProjects } from "bopodev-db";
7
+ import { getCompany, listAgents, listGoals, listProjects } from "bopodev-db";
8
8
  import {
9
9
  resolveAgentMemoryRootPath,
10
10
  resolveAgentOperatingPath,
@@ -117,6 +117,7 @@ function buildReadmeMarkdown(input: {
117
117
  projectRows: { slug: string; name: string; description: string | null }[];
118
118
  skillFileCount: number;
119
119
  taskCount: number;
120
+ goalCount: number;
120
121
  exportedAt: string;
121
122
  }): string {
122
123
  const lines = [
@@ -128,6 +129,7 @@ function buildReadmeMarkdown(input: {
128
129
  "|---------|-------|",
129
130
  `| Agents | ${input.agentRows.length} |`,
130
131
  `| Projects | ${input.projectRows.length} |`,
132
+ `| Goals | ${input.goalCount} |`,
131
133
  `| Skills (files under skills/) | ${input.skillFileCount} |`,
132
134
  `| Scheduled tasks | ${input.taskCount} |`,
133
135
  "",
@@ -167,7 +169,11 @@ export async function buildCompanyExportFileMap(
167
169
  throw new CompanyFileArchiveError("Company not found.");
168
170
  }
169
171
 
170
- const [projects, agents] = await Promise.all([listProjects(db, companyId), listAgents(db, companyId)]);
172
+ const [projects, agents, goalRows] = await Promise.all([
173
+ listProjects(db, companyId),
174
+ listAgents(db, companyId),
175
+ listGoals(db, companyId)
176
+ ]);
171
177
  const loops = await listWorkLoops(db, companyId);
172
178
 
173
179
  const usedSlugs = new Set<string>();
@@ -201,6 +207,10 @@ export async function buildCompanyExportFileMap(
201
207
  providerType: string;
202
208
  heartbeatCron: string;
203
209
  canHireAgents: boolean;
210
+ canAssignAgents: boolean;
211
+ canCreateIssues: boolean;
212
+ bootstrapPrompt: string | null;
213
+ monthlyBudgetUsd: string;
204
214
  }
205
215
  > = {};
206
216
 
@@ -221,7 +231,11 @@ export async function buildCompanyExportFileMap(
221
231
  managerSlug: mgrSlug,
222
232
  providerType: a.providerType,
223
233
  heartbeatCron: a.heartbeatCron,
224
- canHireAgents: Boolean(a.canHireAgents)
234
+ canHireAgents: Boolean(a.canHireAgents),
235
+ canAssignAgents: a.canAssignAgents ?? true,
236
+ canCreateIssues: a.canCreateIssues ?? true,
237
+ bootstrapPrompt: a.bootstrapPrompt?.trim() ? a.bootstrapPrompt : null,
238
+ monthlyBudgetUsd: String(a.monthlyBudgetUsd ?? "100.0000")
225
239
  };
226
240
  }
227
241
 
@@ -264,6 +278,43 @@ export async function buildCompanyExportFileMap(
264
278
  };
265
279
  }
266
280
 
281
+ const goalSlugById = new Map<string, string>();
282
+ const sortedGoals = [...goalRows].sort((a, b) => a.id.localeCompare(b.id));
283
+ for (const g of sortedGoals) {
284
+ const slug = slugify(g.title, usedSlugs);
285
+ goalSlugById.set(g.id, slug);
286
+ }
287
+
288
+ const goalManifest: Record<
289
+ string,
290
+ {
291
+ bopoGoalId: string;
292
+ level: string;
293
+ title: string;
294
+ description: string | null;
295
+ status: string;
296
+ projectSlug: string | null;
297
+ parentGoalSlug: string | null;
298
+ ownerAgentSlug: string | null;
299
+ }
300
+ > = {};
301
+ for (const g of sortedGoals) {
302
+ const slug = goalSlugById.get(g.id)!;
303
+ const projectSlug = g.projectId ? projectSlugById.get(g.projectId) ?? null : null;
304
+ const parentGoalSlug = g.parentGoalId ? goalSlugById.get(g.parentGoalId) ?? null : null;
305
+ const ownerAgentSlug = g.ownerAgentId ? agentSlugById.get(g.ownerAgentId) ?? null : null;
306
+ goalManifest[slug] = {
307
+ bopoGoalId: g.id,
308
+ level: g.level,
309
+ title: g.title,
310
+ description: g.description ?? null,
311
+ status: g.status,
312
+ projectSlug,
313
+ parentGoalSlug,
314
+ ownerAgentSlug
315
+ };
316
+ }
317
+
267
318
  const yamlDoc = {
268
319
  schema: EXPORT_SCHEMA,
269
320
  exportedAt: new Date().toISOString(),
@@ -275,6 +326,7 @@ export async function buildCompanyExportFileMap(
275
326
  },
276
327
  projects: Object.fromEntries(projectEntries.map((p) => [p.slug, { bopoProjectId: p.id, name: p.name, description: p.description, status: p.status }])),
277
328
  agents: agentManifest,
329
+ goals: goalManifest,
278
330
  routines: routineManifest
279
331
  };
280
332
 
@@ -307,6 +359,7 @@ export async function buildCompanyExportFileMap(
307
359
  projectRows: projectEntries.map((p) => ({ slug: p.slug, name: p.name, description: p.description })),
308
360
  skillFileCount,
309
361
  taskCount,
362
+ goalCount: sortedGoals.length,
310
363
  exportedAt: yamlDoc.exportedAt
311
364
  });
312
365