bopodev-api 0.1.33 → 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.
- package/package.json +5 -5
- package/src/app.ts +4 -2
- package/src/assets/starter-packs/customer-support-excellence.zip +0 -0
- package/src/assets/starter-packs/devrel-growth.zip +0 -0
- package/src/assets/starter-packs/product-delivery-trio.zip +0 -0
- package/src/assets/starter-packs/revenue-gtm-b2b.zip +0 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/.bopo.yaml +129 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/COMPANY.md +7 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/README.md +3 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/agents/founder-ceo/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/agents/support-lead/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/agents/support-specialist/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/projects/knowledge-base/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/projects/quality/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/projects/queue/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/skills/kb-article-skeleton/SKILL.md +17 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/skills/ticket-response-playbook/SKILL.md +15 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/tasks/daily-queue-standup/TASK.md +11 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/tasks/kb-gap-sweep/TASK.md +11 -0
- package/src/assets/starter-packs/sources/devrel-growth/.bopo.yaml +128 -0
- package/src/assets/starter-packs/sources/devrel-growth/COMPANY.md +7 -0
- package/src/assets/starter-packs/sources/devrel-growth/README.md +3 -0
- package/src/assets/starter-packs/sources/devrel-growth/agents/content-producer/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/devrel-growth/agents/devrel-lead/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/devrel-growth/agents/founder-ceo/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/devrel-growth/projects/community/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/devrel-growth/projects/docs-education/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/devrel-growth/projects/partners/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/devrel-growth/skills/changelog-to-post/SKILL.md +14 -0
- package/src/assets/starter-packs/sources/devrel-growth/skills/tutorial-outline/SKILL.md +15 -0
- package/src/assets/starter-packs/sources/devrel-growth/tasks/community-health-review/TASK.md +11 -0
- package/src/assets/starter-packs/sources/devrel-growth/tasks/weekly-content-plan/TASK.md +11 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/.bopo.yaml +138 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/COMPANY.md +7 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/README.md +9 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/agents/engineer-ic/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/agents/founder-ceo/HEARTBEAT.md +6 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/agents/product-lead/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/projects/delivery/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/projects/quality/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/projects/strategy/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/skills/issue-triage/SKILL.md +21 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/skills/rca-template/SKILL.md +16 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/tasks/release-hygiene/TASK.md +11 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/tasks/weekly-leadership-sync/TASK.md +11 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/.bopo.yaml +132 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/COMPANY.md +7 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/README.md +3 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/founder-ceo/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/gtm-lead/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/pipeline-owner/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/customer-success/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/deals/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/pipeline/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/skills/discovery-call-brief/SKILL.md +14 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/skills/icp-scoring/SKILL.md +20 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/tasks/pipeline-hygiene/TASK.md +11 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/tasks/weekly-revenue-review/TASK.md +11 -0
- package/src/lib/agent-issue-permissions.ts +56 -0
- package/src/lib/builtin-bopo-skills/bopodev-control-plane.md +7 -0
- package/src/realtime/office-space.ts +7 -0
- package/src/routes/agents.ts +23 -1
- package/src/routes/assistant.ts +40 -1
- package/src/routes/companies.ts +227 -15
- package/src/routes/issues.ts +48 -0
- package/src/routes/plugins.ts +393 -103
- package/src/routes/{loops.ts → routines.ts} +72 -76
- package/src/scripts/onboard-seed.ts +2 -0
- package/src/server.ts +3 -1
- package/src/services/company-assistant-context-snapshot.ts +4 -2
- package/src/services/company-assistant-service.ts +17 -15
- package/src/services/company-file-archive-service.ts +56 -3
- package/src/services/company-file-import-service.ts +210 -31
- package/src/services/governance-service.ts +58 -3
- package/src/services/heartbeat-service/heartbeat-run.ts +7 -0
- package/src/services/plugin-artifact-installer.ts +115 -0
- package/src/services/plugin-artifact-store.ts +28 -0
- package/src/services/plugin-capability-policy.ts +31 -0
- package/src/services/plugin-jobs-service.ts +74 -0
- package/src/services/plugin-manifest-loader.ts +78 -3
- package/src/services/plugin-rpc.ts +102 -0
- package/src/services/plugin-runtime.ts +240 -209
- package/src/services/plugin-worker-host.ts +167 -0
- package/src/services/starter-pack-registry.ts +68 -0
- package/src/services/template-apply-service.ts +3 -1
- package/src/services/template-catalog.ts +29 -0
- package/src/services/work-loop-service/work-loop-service.ts +18 -18
- package/src/shutdown/graceful-shutdown.ts +3 -1
- package/src/worker/scheduler.ts +21 -1
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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, "
|
|
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(
|
|
100
|
+
return sendOk(res, { data: rows.map(serializeRoutine) });
|
|
105
101
|
});
|
|
106
102
|
|
|
107
103
|
router.post("/", async (req, res) => {
|
|
108
|
-
if (!enforcePermission(req, res, "
|
|
104
|
+
if (!enforcePermission(req, res, "routines:write")) {
|
|
109
105
|
return;
|
|
110
106
|
}
|
|
111
|
-
const parsed =
|
|
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
|
|
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: {
|
|
136
|
+
payload: { routineId: row.id, title: row.title }
|
|
141
137
|
});
|
|
142
|
-
return sendOk(res, { data:
|
|
138
|
+
return sendOk(res, { data: serializeRoutine(row) });
|
|
143
139
|
} catch (e) {
|
|
144
|
-
return sendError(res, e instanceof Error ? e.message : "Failed to create
|
|
140
|
+
return sendError(res, e instanceof Error ? e.message : "Failed to create routine.", 422);
|
|
145
141
|
}
|
|
146
142
|
});
|
|
147
143
|
|
|
148
|
-
router.get("/:
|
|
149
|
-
if (!enforcePermission(req, res, "
|
|
144
|
+
router.get("/:routineId", async (req, res) => {
|
|
145
|
+
if (!enforcePermission(req, res, "routines:read")) {
|
|
150
146
|
return;
|
|
151
147
|
}
|
|
152
|
-
const
|
|
153
|
-
const row = await getWorkLoop(ctx.db, req.companyId!,
|
|
148
|
+
const routineId = req.params.routineId;
|
|
149
|
+
const row = await getWorkLoop(ctx.db, req.companyId!, routineId);
|
|
154
150
|
if (!row) {
|
|
155
|
-
return sendError(res, "
|
|
151
|
+
return sendError(res, "Routine not found.", 404);
|
|
156
152
|
}
|
|
157
153
|
const [triggers, recentRuns] = await Promise.all([
|
|
158
|
-
listWorkLoopTriggers(ctx.db, req.companyId!,
|
|
159
|
-
listWorkLoopRuns(ctx.db, req.companyId!,
|
|
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
|
-
...
|
|
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("/:
|
|
171
|
-
if (!enforcePermission(req, res, "
|
|
166
|
+
router.patch("/:routineId", async (req, res) => {
|
|
167
|
+
if (!enforcePermission(req, res, "routines:write")) {
|
|
172
168
|
return;
|
|
173
169
|
}
|
|
174
|
-
const parsed =
|
|
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.
|
|
174
|
+
const row = await updateWorkLoop(ctx.db, req.companyId!, req.params.routineId, parsed.data);
|
|
179
175
|
if (!row) {
|
|
180
|
-
return sendError(res, "
|
|
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:
|
|
188
|
+
return sendOk(res, { data: serializeRoutine(row) });
|
|
193
189
|
});
|
|
194
190
|
|
|
195
|
-
router.post("/:
|
|
196
|
-
if (!enforcePermission(req, res, "
|
|
191
|
+
router.post("/:routineId/run", async (req, res) => {
|
|
192
|
+
if (!enforcePermission(req, res, "routines:run")) {
|
|
197
193
|
return;
|
|
198
194
|
}
|
|
199
|
-
const
|
|
200
|
-
const loop = await getWorkLoop(ctx.db, req.companyId!,
|
|
195
|
+
const routineId = req.params.routineId;
|
|
196
|
+
const loop = await getWorkLoop(ctx.db, req.companyId!, routineId);
|
|
201
197
|
if (!loop) {
|
|
202
|
-
return sendError(res, "
|
|
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:${
|
|
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, "
|
|
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:
|
|
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("/:
|
|
230
|
-
if (!enforcePermission(req, res, "
|
|
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.
|
|
229
|
+
const loop = await getWorkLoop(ctx.db, req.companyId!, req.params.routineId);
|
|
234
230
|
if (!loop) {
|
|
235
|
-
return sendError(res, "
|
|
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.
|
|
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("/:
|
|
243
|
-
if (!enforcePermission(req, res, "
|
|
238
|
+
router.get("/:routineId/activity", async (req, res) => {
|
|
239
|
+
if (!enforcePermission(req, res, "routines:read")) {
|
|
244
240
|
return;
|
|
245
241
|
}
|
|
246
|
-
const
|
|
247
|
-
const loop = await getWorkLoop(ctx.db, req.companyId!,
|
|
242
|
+
const routineId = req.params.routineId;
|
|
243
|
+
const loop = await getWorkLoop(ctx.db, req.companyId!, routineId);
|
|
248
244
|
if (!loop) {
|
|
249
|
-
return sendError(res, "
|
|
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 ===
|
|
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("/:
|
|
266
|
-
if (!enforcePermission(req, res, "
|
|
261
|
+
router.post("/:routineId/triggers", async (req, res) => {
|
|
262
|
+
if (!enforcePermission(req, res, "routines:write")) {
|
|
267
263
|
return;
|
|
268
264
|
}
|
|
269
|
-
const
|
|
270
|
-
const loop = await getWorkLoop(ctx.db, req.companyId!,
|
|
265
|
+
const routineId = req.params.routineId;
|
|
266
|
+
const loop = await getWorkLoop(ctx.db, req.companyId!, routineId);
|
|
271
267
|
if (!loop) {
|
|
272
|
-
return sendError(res, "
|
|
268
|
+
return sendError(res, "Routine not found.", 404);
|
|
273
269
|
}
|
|
274
|
-
const parsed =
|
|
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
|
-
|
|
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
|
-
|
|
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("/:
|
|
311
|
-
if (!enforcePermission(req, res, "
|
|
306
|
+
router.patch("/:routineId/triggers/:triggerId", async (req, res) => {
|
|
307
|
+
if (!enforcePermission(req, res, "routines:write")) {
|
|
312
308
|
return;
|
|
313
309
|
}
|
|
314
|
-
const parsed =
|
|
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.
|
|
314
|
+
const loop = await getWorkLoop(ctx.db, req.companyId!, req.params.routineId);
|
|
319
315
|
if (!loop) {
|
|
320
|
-
return sendError(res, "
|
|
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.
|
|
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("/:
|
|
334
|
-
if (!enforcePermission(req, res, "
|
|
329
|
+
router.delete("/:routineId/triggers/:triggerId", async (req, res) => {
|
|
330
|
+
if (!enforcePermission(req, res, "routines:write")) {
|
|
335
331
|
return;
|
|
336
332
|
}
|
|
337
|
-
const {
|
|
338
|
-
const loop = await getWorkLoop(ctx.db, req.companyId!,
|
|
333
|
+
const { routineId, triggerId } = req.params;
|
|
334
|
+
const loop = await getWorkLoop(ctx.db, req.companyId!, routineId);
|
|
339
335
|
if (!loop) {
|
|
340
|
-
return sendError(res, "
|
|
336
|
+
return sendError(res, "Routine not found.", 404);
|
|
341
337
|
}
|
|
342
|
-
const deleted = await deleteWorkLoopTrigger(ctx.db, req.companyId!,
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: "
|
|
307
|
-
description: "List recurring work
|
|
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: "
|
|
312
|
-
description: "Get one
|
|
313
|
+
name: "get_routine",
|
|
314
|
+
description: "Get one routine by id.",
|
|
313
315
|
inputSchema: {
|
|
314
316
|
type: "object",
|
|
315
|
-
properties: {
|
|
316
|
-
required: ["
|
|
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 "
|
|
600
|
+
case "list_routines": {
|
|
599
601
|
const loops = await listWorkLoops(db, companyId);
|
|
600
|
-
return capToolOutput(loops.map((l) =>
|
|
602
|
+
return capToolOutput(loops.map((l) => serializeRoutineRow(l as unknown as Record<string, unknown>)));
|
|
601
603
|
}
|
|
602
|
-
case "
|
|
603
|
-
const
|
|
604
|
-
const row = await getWorkLoop(db, companyId,
|
|
605
|
-
return capToolOutput(row ?
|
|
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([
|
|
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
|
|