bopodev-api 0.1.34 → 0.1.36
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/lib/instance-paths.ts +5 -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 +82 -3
- package/src/routes/observability.ts +222 -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 +81 -6
- package/src/services/company-file-import-service.ts +221 -31
- package/src/services/company-knowledge-file-service.ts +361 -0
- package/src/services/company-skill-file-service.ts +151 -2
- 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/validation/issue-routes.ts +19 -2
- 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,13 +4,14 @@ 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,
|
|
11
|
+
resolveCompanyKnowledgePath,
|
|
11
12
|
resolveCompanyProjectsWorkspacePath
|
|
12
13
|
} from "../lib/instance-paths";
|
|
13
|
-
import { SKILL_LINK_BASENAME } from "./company-skill-file-service";
|
|
14
|
+
import { SKILL_LINK_BASENAME, SKILL_SIDEBAR_TITLE_BASENAME } from "./company-skill-file-service";
|
|
14
15
|
import { listWorkLoopTriggers, listWorkLoops } from "./work-loop-service/work-loop-service";
|
|
15
16
|
|
|
16
17
|
const EXPORT_SCHEMA = "bopo/company-export/v1";
|
|
@@ -69,7 +70,7 @@ async function walkTextFilesUnder(rootAbs: string, budget: { n: number }): Promi
|
|
|
69
70
|
return;
|
|
70
71
|
}
|
|
71
72
|
const name = ent.name;
|
|
72
|
-
if (name.startsWith(".") && name !== SKILL_LINK_BASENAME) {
|
|
73
|
+
if (name.startsWith(".") && name !== SKILL_LINK_BASENAME && name !== SKILL_SIDEBAR_TITLE_BASENAME) {
|
|
73
74
|
continue;
|
|
74
75
|
}
|
|
75
76
|
const full = join(dir, name);
|
|
@@ -110,13 +111,25 @@ async function walkSkillsDir(companyId: string, budget: { n: number }): Promise<
|
|
|
110
111
|
return out;
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
async function walkKnowledgeDir(companyId: string, budget: { n: number }): Promise<Record<string, string>> {
|
|
115
|
+
const root = resolveCompanyKnowledgePath(companyId);
|
|
116
|
+
const files = await walkTextFilesUnder(root, budget);
|
|
117
|
+
const out: Record<string, string> = {};
|
|
118
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
119
|
+
out[`knowledge/${rel}`] = content;
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
113
124
|
function buildReadmeMarkdown(input: {
|
|
114
125
|
companyName: string;
|
|
115
126
|
slug: string;
|
|
116
127
|
agentRows: { slug: string; name: string; role: string; managerSlug: string | null }[];
|
|
117
128
|
projectRows: { slug: string; name: string; description: string | null }[];
|
|
118
129
|
skillFileCount: number;
|
|
130
|
+
knowledgeFileCount: number;
|
|
119
131
|
taskCount: number;
|
|
132
|
+
goalCount: number;
|
|
120
133
|
exportedAt: string;
|
|
121
134
|
}): string {
|
|
122
135
|
const lines = [
|
|
@@ -128,7 +141,9 @@ function buildReadmeMarkdown(input: {
|
|
|
128
141
|
"|---------|-------|",
|
|
129
142
|
`| Agents | ${input.agentRows.length} |`,
|
|
130
143
|
`| Projects | ${input.projectRows.length} |`,
|
|
144
|
+
`| Goals | ${input.goalCount} |`,
|
|
131
145
|
`| Skills (files under skills/) | ${input.skillFileCount} |`,
|
|
146
|
+
`| Knowledge (files under knowledge/) | ${input.knowledgeFileCount} |`,
|
|
132
147
|
`| Scheduled tasks | ${input.taskCount} |`,
|
|
133
148
|
"",
|
|
134
149
|
"### Agents",
|
|
@@ -167,7 +182,11 @@ export async function buildCompanyExportFileMap(
|
|
|
167
182
|
throw new CompanyFileArchiveError("Company not found.");
|
|
168
183
|
}
|
|
169
184
|
|
|
170
|
-
const [projects, agents] = await Promise.all([
|
|
185
|
+
const [projects, agents, goalRows] = await Promise.all([
|
|
186
|
+
listProjects(db, companyId),
|
|
187
|
+
listAgents(db, companyId),
|
|
188
|
+
listGoals(db, companyId)
|
|
189
|
+
]);
|
|
171
190
|
const loops = await listWorkLoops(db, companyId);
|
|
172
191
|
|
|
173
192
|
const usedSlugs = new Set<string>();
|
|
@@ -201,6 +220,10 @@ export async function buildCompanyExportFileMap(
|
|
|
201
220
|
providerType: string;
|
|
202
221
|
heartbeatCron: string;
|
|
203
222
|
canHireAgents: boolean;
|
|
223
|
+
canAssignAgents: boolean;
|
|
224
|
+
canCreateIssues: boolean;
|
|
225
|
+
bootstrapPrompt: string | null;
|
|
226
|
+
monthlyBudgetUsd: string;
|
|
204
227
|
}
|
|
205
228
|
> = {};
|
|
206
229
|
|
|
@@ -221,7 +244,11 @@ export async function buildCompanyExportFileMap(
|
|
|
221
244
|
managerSlug: mgrSlug,
|
|
222
245
|
providerType: a.providerType,
|
|
223
246
|
heartbeatCron: a.heartbeatCron,
|
|
224
|
-
canHireAgents: Boolean(a.canHireAgents)
|
|
247
|
+
canHireAgents: Boolean(a.canHireAgents),
|
|
248
|
+
canAssignAgents: a.canAssignAgents ?? true,
|
|
249
|
+
canCreateIssues: a.canCreateIssues ?? true,
|
|
250
|
+
bootstrapPrompt: a.bootstrapPrompt?.trim() ? a.bootstrapPrompt : null,
|
|
251
|
+
monthlyBudgetUsd: String(a.monthlyBudgetUsd ?? "100.0000")
|
|
225
252
|
};
|
|
226
253
|
}
|
|
227
254
|
|
|
@@ -264,6 +291,43 @@ export async function buildCompanyExportFileMap(
|
|
|
264
291
|
};
|
|
265
292
|
}
|
|
266
293
|
|
|
294
|
+
const goalSlugById = new Map<string, string>();
|
|
295
|
+
const sortedGoals = [...goalRows].sort((a, b) => a.id.localeCompare(b.id));
|
|
296
|
+
for (const g of sortedGoals) {
|
|
297
|
+
const slug = slugify(g.title, usedSlugs);
|
|
298
|
+
goalSlugById.set(g.id, slug);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const goalManifest: Record<
|
|
302
|
+
string,
|
|
303
|
+
{
|
|
304
|
+
bopoGoalId: string;
|
|
305
|
+
level: string;
|
|
306
|
+
title: string;
|
|
307
|
+
description: string | null;
|
|
308
|
+
status: string;
|
|
309
|
+
projectSlug: string | null;
|
|
310
|
+
parentGoalSlug: string | null;
|
|
311
|
+
ownerAgentSlug: string | null;
|
|
312
|
+
}
|
|
313
|
+
> = {};
|
|
314
|
+
for (const g of sortedGoals) {
|
|
315
|
+
const slug = goalSlugById.get(g.id)!;
|
|
316
|
+
const projectSlug = g.projectId ? projectSlugById.get(g.projectId) ?? null : null;
|
|
317
|
+
const parentGoalSlug = g.parentGoalId ? goalSlugById.get(g.parentGoalId) ?? null : null;
|
|
318
|
+
const ownerAgentSlug = g.ownerAgentId ? agentSlugById.get(g.ownerAgentId) ?? null : null;
|
|
319
|
+
goalManifest[slug] = {
|
|
320
|
+
bopoGoalId: g.id,
|
|
321
|
+
level: g.level,
|
|
322
|
+
title: g.title,
|
|
323
|
+
description: g.description ?? null,
|
|
324
|
+
status: g.status,
|
|
325
|
+
projectSlug,
|
|
326
|
+
parentGoalSlug,
|
|
327
|
+
ownerAgentSlug
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
267
331
|
const yamlDoc = {
|
|
268
332
|
schema: EXPORT_SCHEMA,
|
|
269
333
|
exportedAt: new Date().toISOString(),
|
|
@@ -275,6 +339,7 @@ export async function buildCompanyExportFileMap(
|
|
|
275
339
|
},
|
|
276
340
|
projects: Object.fromEntries(projectEntries.map((p) => [p.slug, { bopoProjectId: p.id, name: p.name, description: p.description, status: p.status }])),
|
|
277
341
|
agents: agentManifest,
|
|
342
|
+
goals: goalManifest,
|
|
278
343
|
routines: routineManifest
|
|
279
344
|
};
|
|
280
345
|
|
|
@@ -298,6 +363,12 @@ export async function buildCompanyExportFileMap(
|
|
|
298
363
|
files[p] = c;
|
|
299
364
|
}
|
|
300
365
|
|
|
366
|
+
const knowledgeFiles = await walkKnowledgeDir(companyId, skillBudget);
|
|
367
|
+
const knowledgeFileCount = Object.keys(knowledgeFiles).length;
|
|
368
|
+
for (const [p, c] of Object.entries(knowledgeFiles)) {
|
|
369
|
+
files[p] = c;
|
|
370
|
+
}
|
|
371
|
+
|
|
301
372
|
const taskCount = Object.keys(routineManifest).length;
|
|
302
373
|
|
|
303
374
|
files["README.md"] = buildReadmeMarkdown({
|
|
@@ -306,7 +377,9 @@ export async function buildCompanyExportFileMap(
|
|
|
306
377
|
agentRows: agentRowsForReadme,
|
|
307
378
|
projectRows: projectEntries.map((p) => ({ slug: p.slug, name: p.name, description: p.description })),
|
|
308
379
|
skillFileCount,
|
|
380
|
+
knowledgeFileCount,
|
|
309
381
|
taskCount,
|
|
382
|
+
goalCount: sortedGoals.length,
|
|
310
383
|
exportedAt: yamlDoc.exportedAt
|
|
311
384
|
});
|
|
312
385
|
|
|
@@ -367,7 +440,9 @@ export async function listCompanyExportManifest(
|
|
|
367
440
|
return Object.entries(files)
|
|
368
441
|
.map(([path, content]): CompanyExportFileEntry => {
|
|
369
442
|
const source: "generated" | "workspace" =
|
|
370
|
-
path.startsWith("agents/") || path.startsWith("skills/")
|
|
443
|
+
path.startsWith("agents/") || path.startsWith("skills/") || path.startsWith("knowledge/")
|
|
444
|
+
? "workspace"
|
|
445
|
+
: "generated";
|
|
371
446
|
return {
|
|
372
447
|
path,
|
|
373
448
|
bytes: Buffer.byteLength(content, "utf8"),
|