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.
- 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
package/src/routes/agents.ts
CHANGED
|
@@ -108,6 +108,8 @@ const UPDATE_AGENT_ALLOWED_KEYS = new Set([
|
|
|
108
108
|
"heartbeatCron",
|
|
109
109
|
"monthlyBudgetUsd",
|
|
110
110
|
"canHireAgents",
|
|
111
|
+
"canAssignAgents",
|
|
112
|
+
"canCreateIssues",
|
|
111
113
|
"runtimeConfig",
|
|
112
114
|
"runtimeCommand",
|
|
113
115
|
"runtimeArgs",
|
|
@@ -120,7 +122,9 @@ const UPDATE_AGENT_ALLOWED_KEYS = new Set([
|
|
|
120
122
|
"interruptGraceSec",
|
|
121
123
|
"runtimeEnv",
|
|
122
124
|
"runPolicy",
|
|
123
|
-
"enabledSkillIds"
|
|
125
|
+
"enabledSkillIds",
|
|
126
|
+
"lucideIconName",
|
|
127
|
+
"avatarSeed"
|
|
124
128
|
]);
|
|
125
129
|
const UPDATE_RUNTIME_CONFIG_ALLOWED_KEYS = new Set([
|
|
126
130
|
"runtimeCommand",
|
|
@@ -485,6 +489,8 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
485
489
|
heartbeatCron: parsed.data.heartbeatCron,
|
|
486
490
|
monthlyBudgetUsd: parsed.data.monthlyBudgetUsd.toFixed(4),
|
|
487
491
|
canHireAgents: parsed.data.canHireAgents,
|
|
492
|
+
canAssignAgents: parsed.data.canAssignAgents,
|
|
493
|
+
canCreateIssues: parsed.data.canCreateIssues,
|
|
488
494
|
...runtimeConfigToDb(runtimeConfig),
|
|
489
495
|
initialState: runtimeConfigToStateBlobPatch(runtimeConfig)
|
|
490
496
|
});
|
|
@@ -537,6 +543,12 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
537
543
|
if (parsed.data.canHireAgents !== undefined) {
|
|
538
544
|
forbiddenFieldUpdates.push("canHireAgents");
|
|
539
545
|
}
|
|
546
|
+
if (parsed.data.canAssignAgents !== undefined) {
|
|
547
|
+
forbiddenFieldUpdates.push("canAssignAgents");
|
|
548
|
+
}
|
|
549
|
+
if (parsed.data.canCreateIssues !== undefined) {
|
|
550
|
+
forbiddenFieldUpdates.push("canCreateIssues");
|
|
551
|
+
}
|
|
540
552
|
if (parsed.data.status !== undefined) {
|
|
541
553
|
forbiddenFieldUpdates.push("status");
|
|
542
554
|
}
|
|
@@ -616,10 +628,18 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
616
628
|
if (requiresRuntimeCwd(effectiveProviderType) && hasText(effectiveRuntimeCwd)) {
|
|
617
629
|
await mkdir(effectiveRuntimeCwd, { recursive: true });
|
|
618
630
|
}
|
|
631
|
+
const nextLucideIconName =
|
|
632
|
+
parsed.data.lucideIconName === undefined ? undefined : parsed.data.lucideIconName.trim();
|
|
633
|
+
|
|
634
|
+
const nextAvatarSeed =
|
|
635
|
+
parsed.data.avatarSeed === undefined ? undefined : parsed.data.avatarSeed.trim();
|
|
636
|
+
|
|
619
637
|
const agent = await updateAgent(ctx.db, {
|
|
620
638
|
companyId: req.companyId!,
|
|
621
639
|
id: req.params.agentId,
|
|
622
640
|
managerAgentId: parsed.data.managerAgentId,
|
|
641
|
+
lucideIconName: nextLucideIconName,
|
|
642
|
+
avatarSeed: nextAvatarSeed,
|
|
623
643
|
role:
|
|
624
644
|
parsed.data.role !== undefined || parsed.data.roleKey !== undefined || parsed.data.title !== undefined
|
|
625
645
|
? resolveAgentRoleText(
|
|
@@ -639,6 +659,8 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
639
659
|
monthlyBudgetUsd:
|
|
640
660
|
typeof parsed.data.monthlyBudgetUsd === "number" ? parsed.data.monthlyBudgetUsd.toFixed(4) : undefined,
|
|
641
661
|
canHireAgents: parsed.data.canHireAgents,
|
|
662
|
+
canAssignAgents: parsed.data.canAssignAgents,
|
|
663
|
+
canCreateIssues: parsed.data.canCreateIssues,
|
|
642
664
|
...runtimeConfigToDb(nextRuntime),
|
|
643
665
|
stateBlob: runtimeConfigToStateBlobPatch(nextRuntime)
|
|
644
666
|
});
|
package/src/routes/assistant.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { Router } from "express";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
createAssistantThread,
|
|
5
|
+
deleteAssistantThread,
|
|
5
6
|
getAssistantThreadById,
|
|
6
7
|
getOrCreateAssistantThread,
|
|
7
|
-
listAssistantMessages
|
|
8
|
+
listAssistantMessages,
|
|
9
|
+
listAssistantThreadsForCompany
|
|
8
10
|
} from "bopodev-db";
|
|
9
11
|
import type { AppContext } from "../context";
|
|
10
12
|
import { sendError, sendOk } from "../http";
|
|
@@ -30,6 +32,20 @@ export function createAssistantRouter(ctx: AppContext) {
|
|
|
30
32
|
return sendOk(res, { brains: listAskAssistantBrains() });
|
|
31
33
|
});
|
|
32
34
|
|
|
35
|
+
router.get("/threads", async (req, res) => {
|
|
36
|
+
const companyId = req.companyId!;
|
|
37
|
+
const rawLimit = Number(req.query.limit ?? 50);
|
|
38
|
+
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.floor(rawLimit), 1), 100) : 50;
|
|
39
|
+
const rows = await listAssistantThreadsForCompany(ctx.db, companyId, limit);
|
|
40
|
+
return sendOk(res, {
|
|
41
|
+
threads: rows.map((t) => ({
|
|
42
|
+
id: t.id,
|
|
43
|
+
updatedAt: t.updatedAt.toISOString(),
|
|
44
|
+
preview: t.previewBody ? truncateAssistantPreview(t.previewBody) : null
|
|
45
|
+
}))
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
33
49
|
router.get("/messages", async (req, res) => {
|
|
34
50
|
const companyId = req.companyId!;
|
|
35
51
|
const qThread =
|
|
@@ -97,6 +113,19 @@ export function createAssistantRouter(ctx: AppContext) {
|
|
|
97
113
|
return sendOk(res, { threadId: thread.id });
|
|
98
114
|
});
|
|
99
115
|
|
|
116
|
+
router.delete("/threads/:threadId", async (req, res) => {
|
|
117
|
+
const companyId = req.companyId!;
|
|
118
|
+
const threadId = typeof req.params.threadId === "string" ? req.params.threadId.trim() : "";
|
|
119
|
+
if (!threadId) {
|
|
120
|
+
return sendError(res, "threadId is required.", 422);
|
|
121
|
+
}
|
|
122
|
+
const ok = await deleteAssistantThread(ctx.db, companyId, threadId);
|
|
123
|
+
if (!ok) {
|
|
124
|
+
return sendError(res, "Chat thread not found.", 404);
|
|
125
|
+
}
|
|
126
|
+
return sendOk(res, { deleted: true });
|
|
127
|
+
});
|
|
128
|
+
|
|
100
129
|
return router;
|
|
101
130
|
}
|
|
102
131
|
|
|
@@ -107,3 +136,13 @@ function safeJsonParse(raw: string): unknown {
|
|
|
107
136
|
return null;
|
|
108
137
|
}
|
|
109
138
|
}
|
|
139
|
+
|
|
140
|
+
const ASSISTANT_THREAD_PREVIEW_MAX = 120;
|
|
141
|
+
|
|
142
|
+
function truncateAssistantPreview(body: string): string {
|
|
143
|
+
const singleLine = body.replace(/\s+/g, " ").trim();
|
|
144
|
+
if (singleLine.length <= ASSISTANT_THREAD_PREVIEW_MAX) {
|
|
145
|
+
return singleLine;
|
|
146
|
+
}
|
|
147
|
+
return `${singleLine.slice(0, ASSISTANT_THREAD_PREVIEW_MAX - 1)}…`;
|
|
148
|
+
}
|
package/src/routes/companies.ts
CHANGED
|
@@ -3,8 +3,18 @@ import type { NextFunction, Request, Response } from "express";
|
|
|
3
3
|
import { Router } from "express";
|
|
4
4
|
import multer from "multer";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { CompanySchema } from "bopodev-contracts";
|
|
7
|
-
import {
|
|
6
|
+
import { CompanySchema, TemplateManifestSchema } from "bopodev-contracts";
|
|
7
|
+
import {
|
|
8
|
+
createAgent,
|
|
9
|
+
createCompany,
|
|
10
|
+
deleteCompany,
|
|
11
|
+
getCurrentTemplateVersion,
|
|
12
|
+
getTemplateBySlug,
|
|
13
|
+
listAgents,
|
|
14
|
+
listCompanies,
|
|
15
|
+
updateAgent,
|
|
16
|
+
updateCompany
|
|
17
|
+
} from "bopodev-db";
|
|
8
18
|
import type { AppContext } from "../context";
|
|
9
19
|
import { sendError, sendOk, sendOkValidated } from "../http";
|
|
10
20
|
import { normalizeRuntimeConfig, resolveRuntimeModelForProvider, runtimeConfigToDb, runtimeConfigToStateBlobPatch } from "../lib/agent-config";
|
|
@@ -19,10 +29,22 @@ import {
|
|
|
19
29
|
pipeCompanyExportZip,
|
|
20
30
|
readCompanyExportFileText
|
|
21
31
|
} from "../services/company-file-archive-service";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
import {
|
|
33
|
+
assertManifestHasCeoAgent,
|
|
34
|
+
CompanyFileImportError,
|
|
35
|
+
importCompanyFromZipBuffer,
|
|
36
|
+
parseCompanyZipBuffer,
|
|
37
|
+
seedOperationalDataFromPackage,
|
|
38
|
+
summarizeCompanyPackageForPreview
|
|
39
|
+
} from "../services/company-file-import-service";
|
|
40
|
+
import { ensureBuiltinPluginsRegistered } from "../services/plugin-runtime";
|
|
41
|
+
import { listStarterPackMetadata, readStarterPackZipBuffer, resolveStarterPackDefinition } from "../services/starter-pack-registry";
|
|
42
|
+
import { TemplateApplyError, applyTemplateManifest } from "../services/template-apply-service";
|
|
43
|
+
import {
|
|
44
|
+
ensureCompanyBuiltinTemplateDefaults,
|
|
45
|
+
getBuiltinStarterTemplateBySlug,
|
|
46
|
+
type BuiltinStarterTemplateDefinition
|
|
47
|
+
} from "../services/template-catalog";
|
|
26
48
|
|
|
27
49
|
const zipUpload = multer({
|
|
28
50
|
storage: multer.memoryStorage(),
|
|
@@ -43,7 +65,8 @@ const createCompanySchema = z.object({
|
|
|
43
65
|
providerType: z
|
|
44
66
|
.enum(["codex", "claude_code", "cursor", "gemini_cli", "opencode", "openai_api", "anthropic_api", "http", "shell"])
|
|
45
67
|
.optional(),
|
|
46
|
-
runtimeModel: z.string().optional()
|
|
68
|
+
runtimeModel: z.string().optional(),
|
|
69
|
+
starterPackId: z.string().min(1).optional()
|
|
47
70
|
});
|
|
48
71
|
|
|
49
72
|
const updateCompanySchema = z
|
|
@@ -73,6 +96,36 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
73
96
|
);
|
|
74
97
|
});
|
|
75
98
|
|
|
99
|
+
router.get("/starter-packs", requireBoardRole, async (_req, res) => {
|
|
100
|
+
return sendOk(res, { starterPacks: listStarterPackMetadata() });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
router.post("/import/files/preview", requireBoardRole, zipUpload.single("archive"), async (req, res) => {
|
|
104
|
+
const file = req.file;
|
|
105
|
+
if (!file?.buffer) {
|
|
106
|
+
return sendError(res, 'Upload a .zip file in field "archive".', 422);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const parsed = parseCompanyZipBuffer(file.buffer);
|
|
110
|
+
const summary = summarizeCompanyPackageForPreview(parsed);
|
|
111
|
+
const warnings: string[] = [];
|
|
112
|
+
if (!summary.hasCeo) {
|
|
113
|
+
warnings.push("No agent with roleKey 'ceo' found; the company may not be ready to run until you add a CEO.");
|
|
114
|
+
}
|
|
115
|
+
return sendOk(res, { ok: true, ...summary, errors: [] as string[], warnings });
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const message = err instanceof CompanyFileImportError ? err.message : String(err);
|
|
118
|
+
return sendOk(res, {
|
|
119
|
+
ok: false,
|
|
120
|
+
companyName: "",
|
|
121
|
+
counts: { projects: 0, agents: 0, goals: 0, routines: 0, skillFiles: 0 },
|
|
122
|
+
hasCeo: false,
|
|
123
|
+
errors: [message],
|
|
124
|
+
warnings: [] as string[]
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
76
129
|
router.post("/import/files", requireBoardRole, zipUpload.single("archive"), async (req, res) => {
|
|
77
130
|
const file = req.file;
|
|
78
131
|
if (!file?.buffer) {
|
|
@@ -173,11 +226,11 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
173
226
|
if (!canAccessCompany(req, companyId)) {
|
|
174
227
|
return sendError(res, "Actor does not have access to this company.", 403);
|
|
175
228
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
229
|
+
return sendError(
|
|
230
|
+
res,
|
|
231
|
+
"JSON company export was removed. Use POST /companies/:companyId/export/files/zip for the portable company zip.",
|
|
232
|
+
410
|
|
233
|
+
);
|
|
181
234
|
});
|
|
182
235
|
|
|
183
236
|
router.post("/", requireBoardRole, async (req, res) => {
|
|
@@ -185,14 +238,142 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
185
238
|
if (!parsed.success) {
|
|
186
239
|
return sendError(res, parsed.error.message, 422);
|
|
187
240
|
}
|
|
188
|
-
const company = await createCompany(ctx.db, parsed.data);
|
|
189
241
|
const providerType =
|
|
190
242
|
parseAgentProvider(parsed.data.providerType) ??
|
|
191
243
|
parseAgentProvider(process.env[DEFAULT_AGENT_PROVIDER_ENV]) ??
|
|
192
244
|
"shell";
|
|
245
|
+
const requestedModel = parsed.data.runtimeModel?.trim() || process.env[DEFAULT_AGENT_MODEL_ENV]?.trim() || undefined;
|
|
246
|
+
|
|
247
|
+
const companyInput = {
|
|
248
|
+
name: parsed.data.name,
|
|
249
|
+
mission: parsed.data.mission ?? null
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (parsed.data.starterPackId?.trim()) {
|
|
253
|
+
const packId = parsed.data.starterPackId.trim();
|
|
254
|
+
const builtinTemplate = getBuiltinStarterTemplateBySlug(packId);
|
|
255
|
+
const zipPack = resolveStarterPackDefinition(packId);
|
|
256
|
+
if (!builtinTemplate && !zipPack) {
|
|
257
|
+
return sendError(res, `Unknown starter pack: ${packId}`, 422);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const company = await createCompany(ctx.db, companyInput);
|
|
261
|
+
await ensureCompanyBuiltinTemplateDefaults(ctx.db, company.id);
|
|
262
|
+
await ensureBuiltinPluginsRegistered(ctx.db, [company.id]);
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
if (builtinTemplate) {
|
|
266
|
+
const templateRow = await getTemplateBySlug(ctx.db, company.id, packId);
|
|
267
|
+
if (!templateRow) {
|
|
268
|
+
return sendError(res, `Starter template '${packId}' was not registered.`, 500);
|
|
269
|
+
}
|
|
270
|
+
const templateVersion = await getCurrentTemplateVersion(ctx.db, company.id, templateRow.id);
|
|
271
|
+
if (!templateVersion) {
|
|
272
|
+
return sendError(res, `Starter template '${packId}' has no current version.`, 500);
|
|
273
|
+
}
|
|
274
|
+
let manifestRaw: unknown;
|
|
275
|
+
try {
|
|
276
|
+
manifestRaw = JSON.parse(templateVersion.manifestJson) as unknown;
|
|
277
|
+
} catch {
|
|
278
|
+
return sendError(res, `Starter template '${packId}' has invalid manifest JSON.`, 500);
|
|
279
|
+
}
|
|
280
|
+
const manifestParsed = TemplateManifestSchema.safeParse(manifestRaw);
|
|
281
|
+
if (!manifestParsed.success) {
|
|
282
|
+
return sendError(res, `Starter template '${packId}' has invalid manifest: ${manifestParsed.error.message}`, 422);
|
|
283
|
+
}
|
|
284
|
+
const variables = buildStarterTemplateVariables(builtinTemplate, companyInput);
|
|
285
|
+
await applyTemplateManifest(ctx.db, {
|
|
286
|
+
companyId: company.id,
|
|
287
|
+
templateId: templateRow.id,
|
|
288
|
+
templateVersion: templateVersion.version,
|
|
289
|
+
templateVersionId: templateVersion.id,
|
|
290
|
+
manifest: manifestParsed.data,
|
|
291
|
+
variables
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
let packBuffer: Buffer;
|
|
295
|
+
try {
|
|
296
|
+
packBuffer = await readStarterPackZipBuffer(packId);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
return sendError(res, `Failed to read starter pack: ${String(err)}`, 500);
|
|
299
|
+
}
|
|
300
|
+
let parsedPackage;
|
|
301
|
+
try {
|
|
302
|
+
parsedPackage = parseCompanyZipBuffer(packBuffer);
|
|
303
|
+
assertManifestHasCeoAgent(parsedPackage.doc);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
const message = err instanceof CompanyFileImportError ? err.message : String(err);
|
|
306
|
+
return sendError(res, message, 422);
|
|
307
|
+
}
|
|
308
|
+
await seedOperationalDataFromPackage(ctx.db, company.id, parsedPackage);
|
|
309
|
+
}
|
|
310
|
+
} catch (err) {
|
|
311
|
+
const message =
|
|
312
|
+
err instanceof TemplateApplyError
|
|
313
|
+
? err.message
|
|
314
|
+
: err instanceof CompanyFileImportError
|
|
315
|
+
? err.message
|
|
316
|
+
: String(err);
|
|
317
|
+
return sendError(res, message, 422);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const agents = await listAgents(ctx.db, company.id);
|
|
321
|
+
const rk = (a: (typeof agents)[number]) => (a.roleKey ?? "").toLowerCase();
|
|
322
|
+
const leaderAgent =
|
|
323
|
+
agents.find((a) => rk(a) === "ceo") ??
|
|
324
|
+
agents.find((a) => rk(a) === "cmo") ??
|
|
325
|
+
agents.find((a) => a.canHireAgents) ??
|
|
326
|
+
agents[0] ??
|
|
327
|
+
null;
|
|
328
|
+
if (!leaderAgent) {
|
|
329
|
+
return sendError(res, "Starter did not yield an agent to attach the selected CEO runtime to.", 422);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const defaultRuntimeCwd = await resolveDefaultRuntimeCwdForCompany(ctx.db, company.id);
|
|
333
|
+
const resolvedRuntimeModel = resolveRuntimeModelForProvider(
|
|
334
|
+
providerType,
|
|
335
|
+
await resolveOpencodeRuntimeModel(
|
|
336
|
+
providerType,
|
|
337
|
+
normalizeRuntimeConfig({
|
|
338
|
+
defaultRuntimeCwd,
|
|
339
|
+
runtimeConfig: {
|
|
340
|
+
runtimeModel: requestedModel,
|
|
341
|
+
runtimeEnv: resolveSeedRuntimeEnv(providerType)
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
const bootstrapPrompt = leaderAgent.bootstrapPrompt?.trim()
|
|
347
|
+
? leaderAgent.bootstrapPrompt
|
|
348
|
+
: buildDefaultCeoBootstrapPrompt();
|
|
349
|
+
const defaultRuntimeConfig = normalizeRuntimeConfig({
|
|
350
|
+
defaultRuntimeCwd,
|
|
351
|
+
runtimeConfig: {
|
|
352
|
+
runtimeModel: resolvedRuntimeModel,
|
|
353
|
+
bootstrapPrompt,
|
|
354
|
+
runtimeEnv: resolveSeedRuntimeEnv(providerType),
|
|
355
|
+
...(providerType === "shell"
|
|
356
|
+
? {
|
|
357
|
+
runtimeCommand: "echo",
|
|
358
|
+
runtimeArgs: ["ceo bootstrap heartbeat"]
|
|
359
|
+
}
|
|
360
|
+
: {})
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
await updateAgent(ctx.db, {
|
|
364
|
+
companyId: company.id,
|
|
365
|
+
id: leaderAgent.id,
|
|
366
|
+
providerType,
|
|
367
|
+
...runtimeConfigToDb(defaultRuntimeConfig),
|
|
368
|
+
stateBlob: runtimeConfigToStateBlobPatch(defaultRuntimeConfig)
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
return sendOk(res, company);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const company = await createCompany(ctx.db, companyInput);
|
|
193
375
|
const defaultRuntimeCwd = await resolveDefaultRuntimeCwdForCompany(ctx.db, company.id);
|
|
194
376
|
await mkdir(defaultRuntimeCwd, { recursive: true });
|
|
195
|
-
const requestedModel = parsed.data.runtimeModel?.trim() || process.env[DEFAULT_AGENT_MODEL_ENV]?.trim() || undefined;
|
|
196
377
|
const resolvedRuntimeModel = resolveRuntimeModelForProvider(
|
|
197
378
|
providerType,
|
|
198
379
|
await resolveOpencodeRuntimeModel(
|
|
@@ -232,10 +413,12 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
232
413
|
heartbeatCron: "*/5 * * * *",
|
|
233
414
|
monthlyBudgetUsd: "100.0000",
|
|
234
415
|
canHireAgents: true,
|
|
416
|
+
canAssignAgents: true,
|
|
417
|
+
canCreateIssues: true,
|
|
235
418
|
...runtimeConfigToDb(defaultRuntimeConfig),
|
|
236
419
|
initialState: runtimeConfigToStateBlobPatch(defaultRuntimeConfig)
|
|
237
420
|
});
|
|
238
|
-
await
|
|
421
|
+
await ensureBuiltinPluginsRegistered(ctx.db, [company.id]);
|
|
239
422
|
await ensureCompanyBuiltinTemplateDefaults(ctx.db, company.id);
|
|
240
423
|
return sendOk(res, company);
|
|
241
424
|
});
|
|
@@ -272,6 +455,35 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
272
455
|
return router;
|
|
273
456
|
}
|
|
274
457
|
|
|
458
|
+
function buildStarterTemplateVariables(
|
|
459
|
+
definition: BuiltinStarterTemplateDefinition,
|
|
460
|
+
input: { name: string; mission: string | null }
|
|
461
|
+
): Record<string, unknown> {
|
|
462
|
+
const name = input.name.trim();
|
|
463
|
+
const missionTail = (input.mission ?? "").trim() || name;
|
|
464
|
+
const defaults: Record<string, string> = {
|
|
465
|
+
brandName: name,
|
|
466
|
+
productName: name,
|
|
467
|
+
targetAudience: missionTail,
|
|
468
|
+
primaryChannel: "LinkedIn"
|
|
469
|
+
};
|
|
470
|
+
const out: Record<string, unknown> = {};
|
|
471
|
+
for (const v of definition.variables) {
|
|
472
|
+
const key = v.key;
|
|
473
|
+
if (defaults[key] !== undefined) {
|
|
474
|
+
out[key] = defaults[key]!;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const dv = v.defaultValue;
|
|
478
|
+
if (dv !== undefined && dv !== null && String(dv).length > 0) {
|
|
479
|
+
out[key] = dv;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
out[key] = missionTail;
|
|
483
|
+
}
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
|
|
275
487
|
function requireCompanyWriteAccess(req: Request, res: Response, next: NextFunction) {
|
|
276
488
|
const targetCompanyId = readCompanyIdParam(req);
|
|
277
489
|
if (!targetCompanyId) {
|
package/src/routes/issues.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
deleteIssue,
|
|
17
17
|
desc,
|
|
18
18
|
eq,
|
|
19
|
+
getCompanyAgent,
|
|
19
20
|
getIssue,
|
|
20
21
|
heartbeatRuns,
|
|
21
22
|
getIssueAttachment,
|
|
@@ -40,6 +41,11 @@ import {
|
|
|
40
41
|
type CommentRecipientInput,
|
|
41
42
|
type PersistedCommentRecipient
|
|
42
43
|
} from "../lib/comment-recipients";
|
|
44
|
+
import {
|
|
45
|
+
agentIssueCreateAssigneeForbiddenMessage,
|
|
46
|
+
agentIssueCreateForbiddenMessage,
|
|
47
|
+
agentIssuePutAssigneeChangeForbiddenMessage
|
|
48
|
+
} from "../lib/agent-issue-permissions";
|
|
43
49
|
import { isInsidePath, normalizeCompanyWorkspacePath, resolveProjectWorkspacePath } from "../lib/instance-paths";
|
|
44
50
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
45
51
|
import { enforcePermission } from "../middleware/request-actor";
|
|
@@ -170,6 +176,28 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
170
176
|
if (!parsed.success) {
|
|
171
177
|
return sendError(res, parsed.error.message, 422);
|
|
172
178
|
}
|
|
179
|
+
if (req.actor?.type === "agent") {
|
|
180
|
+
const agentRow = await getCompanyAgent(ctx.db, req.companyId!, req.actor.id);
|
|
181
|
+
if (!agentRow) {
|
|
182
|
+
return sendError(res, "Agent not found.", 403);
|
|
183
|
+
}
|
|
184
|
+
const orchestration = {
|
|
185
|
+
canAssignAgents: agentRow.canAssignAgents ?? true,
|
|
186
|
+
canCreateIssues: agentRow.canCreateIssues ?? true
|
|
187
|
+
};
|
|
188
|
+
const createDeny = agentIssueCreateForbiddenMessage(orchestration);
|
|
189
|
+
if (createDeny) {
|
|
190
|
+
return sendError(res, createDeny, 403);
|
|
191
|
+
}
|
|
192
|
+
const assignDeny = agentIssueCreateAssigneeForbiddenMessage(
|
|
193
|
+
orchestration,
|
|
194
|
+
req.actor.id,
|
|
195
|
+
parsed.data.assigneeAgentId ?? null
|
|
196
|
+
);
|
|
197
|
+
if (assignDeny) {
|
|
198
|
+
return sendError(res, assignDeny, 403);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
173
201
|
if (parsed.data.assigneeAgentId) {
|
|
174
202
|
const assignmentValidation = await validateIssueAssignmentScope(
|
|
175
203
|
ctx,
|
|
@@ -538,6 +566,26 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
538
566
|
if (!existingIssue) {
|
|
539
567
|
return sendError(res, "Issue not found.", 404);
|
|
540
568
|
}
|
|
569
|
+
if (req.actor?.type === "agent" && parsed.data.assigneeAgentId !== undefined) {
|
|
570
|
+
const agentRow = await getCompanyAgent(ctx.db, req.companyId!, req.actor.id);
|
|
571
|
+
if (!agentRow) {
|
|
572
|
+
return sendError(res, "Agent not found.", 403);
|
|
573
|
+
}
|
|
574
|
+
const orchestration = {
|
|
575
|
+
canAssignAgents: agentRow.canAssignAgents ?? true,
|
|
576
|
+
canCreateIssues: agentRow.canCreateIssues ?? true
|
|
577
|
+
};
|
|
578
|
+
const prevAssignee = existingIssue.assigneeAgentId;
|
|
579
|
+
const putDeny = agentIssuePutAssigneeChangeForbiddenMessage(
|
|
580
|
+
orchestration,
|
|
581
|
+
req.actor.id,
|
|
582
|
+
prevAssignee,
|
|
583
|
+
parsed.data.assigneeAgentId!
|
|
584
|
+
);
|
|
585
|
+
if (putDeny) {
|
|
586
|
+
return sendError(res, putDeny, 403);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
541
589
|
const effectiveProjectId = parsed.data.projectId ?? existingIssue.projectId;
|
|
542
590
|
const effectiveAssigneeAgentId =
|
|
543
591
|
parsed.data.assigneeAgentId === undefined ? existingIssue.assigneeAgentId : parsed.data.assigneeAgentId;
|