bopodev-api 0.1.29 → 0.1.31

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.
@@ -16,7 +16,18 @@ import type { AppContext } from "../context";
16
16
  import { sendError, sendOk } from "../http";
17
17
  import { resolveRunArtifactAbsolutePath } from "../lib/run-artifact-paths";
18
18
  import { requireCompanyScope } from "../middleware/company-scope";
19
- import { listAgentMemoryFiles, loadAgentMemoryContext, readAgentMemoryFile } from "../services/memory-file-service";
19
+ import { enforcePermission } from "../middleware/request-actor";
20
+ import {
21
+ listAgentOperatingMarkdownFiles,
22
+ readAgentOperatingFile,
23
+ writeAgentOperatingFile
24
+ } from "../services/agent-operating-file-service";
25
+ import {
26
+ listAgentMemoryFiles,
27
+ loadAgentMemoryContext,
28
+ readAgentMemoryFile,
29
+ writeAgentMemoryFile
30
+ } from "../services/memory-file-service";
20
31
 
21
32
  export function createObservabilityRouter(ctx: AppContext) {
22
33
  const router = Router();
@@ -259,6 +270,117 @@ export function createObservabilityRouter(ctx: AppContext) {
259
270
  }
260
271
  });
261
272
 
273
+ router.put("/memory/:agentId/file", async (req, res) => {
274
+ if (!enforcePermission(req, res, "agents:write")) {
275
+ return;
276
+ }
277
+ const companyId = req.companyId!;
278
+ const agentId = req.params.agentId;
279
+ const relativePath = typeof req.query.path === "string" ? req.query.path.trim() : "";
280
+ if (!relativePath) {
281
+ return sendError(res, "Query parameter 'path' is required.", 422);
282
+ }
283
+ const body = req.body as { content?: unknown };
284
+ if (typeof body?.content !== "string") {
285
+ return sendError(res, "Expected JSON body with string 'content'.", 422);
286
+ }
287
+ const agents = await listAgents(ctx.db, companyId);
288
+ if (!agents.some((entry) => entry.id === agentId)) {
289
+ return sendError(res, "Agent not found", 404);
290
+ }
291
+ try {
292
+ const result = await writeAgentMemoryFile({
293
+ companyId,
294
+ agentId,
295
+ relativePath,
296
+ content: body.content
297
+ });
298
+ return sendOk(res, result);
299
+ } catch (error) {
300
+ return sendError(res, String(error), 422);
301
+ }
302
+ });
303
+
304
+ router.get("/agent-operating/:agentId/files", async (req, res) => {
305
+ const companyId = req.companyId!;
306
+ const agentId = req.params.agentId;
307
+ const agents = await listAgents(ctx.db, companyId);
308
+ if (!agents.some((entry) => entry.id === agentId)) {
309
+ return sendError(res, "Agent not found", 404);
310
+ }
311
+ const rawLimit = Number(req.query.limit ?? 100);
312
+ const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.floor(rawLimit), 1), 500) : 100;
313
+ try {
314
+ const files = await listAgentOperatingMarkdownFiles({
315
+ companyId,
316
+ agentId,
317
+ maxFiles: limit
318
+ });
319
+ return sendOk(res, {
320
+ items: files.map((file) => ({
321
+ relativePath: file.relativePath,
322
+ path: file.path
323
+ }))
324
+ });
325
+ } catch (error) {
326
+ return sendError(res, String(error), 422);
327
+ }
328
+ });
329
+
330
+ router.get("/agent-operating/:agentId/file", async (req, res) => {
331
+ const companyId = req.companyId!;
332
+ const agentId = req.params.agentId;
333
+ const relativePath = typeof req.query.path === "string" ? req.query.path.trim() : "";
334
+ if (!relativePath) {
335
+ return sendError(res, "Query parameter 'path' is required.", 422);
336
+ }
337
+ const agents = await listAgents(ctx.db, companyId);
338
+ if (!agents.some((entry) => entry.id === agentId)) {
339
+ return sendError(res, "Agent not found", 404);
340
+ }
341
+ try {
342
+ const file = await readAgentOperatingFile({
343
+ companyId,
344
+ agentId,
345
+ relativePath
346
+ });
347
+ return sendOk(res, file);
348
+ } catch (error) {
349
+ return sendError(res, String(error), 422);
350
+ }
351
+ });
352
+
353
+ router.put("/agent-operating/:agentId/file", async (req, res) => {
354
+ if (!enforcePermission(req, res, "agents:write")) {
355
+ return;
356
+ }
357
+ const companyId = req.companyId!;
358
+ const agentId = req.params.agentId;
359
+ const relativePath = typeof req.query.path === "string" ? req.query.path.trim() : "";
360
+ if (!relativePath) {
361
+ return sendError(res, "Query parameter 'path' is required.", 422);
362
+ }
363
+ const body = req.body as { content?: unknown };
364
+ if (typeof body?.content !== "string") {
365
+ return sendError(res, "Expected JSON body with string 'content'.", 422);
366
+ }
367
+ const agents = await listAgents(ctx.db, companyId);
368
+ if (!agents.some((entry) => entry.id === agentId)) {
369
+ return sendError(res, "Agent not found", 404);
370
+ }
371
+ try {
372
+ const result = await writeAgentOperatingFile({
373
+ companyId,
374
+ agentId,
375
+ relativePath,
376
+ content: body.content
377
+ });
378
+ return sendOk(res, result);
379
+ } catch (error) {
380
+ return sendError(res, String(error), 422);
381
+ }
382
+ });
383
+
262
384
  router.get("/memory/:agentId/context-preview", async (req, res) => {
263
385
  const companyId = req.companyId!;
264
386
  const agentId = req.params.agentId;
@@ -50,7 +50,16 @@ const DEFAULT_COMPANY_ID_ENV = "BOPO_DEFAULT_COMPANY_ID";
50
50
  const DEFAULT_AGENT_PROVIDER_ENV = "BOPO_DEFAULT_AGENT_PROVIDER";
51
51
  const DEFAULT_AGENT_MODEL_ENV = "BOPO_DEFAULT_AGENT_MODEL";
52
52
  const DEFAULT_TEMPLATE_ENV = "BOPO_DEFAULT_TEMPLATE_ID";
53
- type AgentProvider = "codex" | "claude_code" | "cursor" | "gemini_cli" | "opencode" | "openai_api" | "anthropic_api" | "shell";
53
+ type AgentProvider =
54
+ | "codex"
55
+ | "claude_code"
56
+ | "cursor"
57
+ | "gemini_cli"
58
+ | "opencode"
59
+ | "openai_api"
60
+ | "anthropic_api"
61
+ | "openclaw_gateway"
62
+ | "shell";
54
63
  const CEO_BOOTSTRAP_SUMMARY = "ceo bootstrap heartbeat";
55
64
  const STARTUP_PROJECT_NAME = "Leadership Setup";
56
65
  const CEO_STARTUP_TASK_TITLE = "Set up CEO operating files and hire founding engineer";
@@ -130,6 +139,8 @@ export async function ensureOnboardingSeed(input: {
130
139
  role: "CEO",
131
140
  roleKey: "ceo",
132
141
  title: "CEO",
142
+ capabilities:
143
+ "Company leadership: priorities, hiring, governance, and aligning agents to mission and budget.",
133
144
  name: "CEO",
134
145
  providerType: agentProvider,
135
146
  heartbeatCron: "*/5 * * * *",
@@ -399,6 +410,7 @@ function parseAgentProvider(value: unknown): AgentProvider | null {
399
410
  value === "opencode" ||
400
411
  value === "openai_api" ||
401
412
  value === "anthropic_api" ||
413
+ value === "openclaw_gateway" ||
402
414
  value === "shell"
403
415
  ) {
404
416
  return value;
@@ -0,0 +1,116 @@
1
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, join, relative, resolve } from "node:path";
3
+ import { isInsidePath, resolveAgentOperatingPath } from "../lib/instance-paths";
4
+
5
+ const MAX_OBSERVABILITY_FILES = 200;
6
+ const MAX_OBSERVABILITY_FILE_BYTES = 512 * 1024;
7
+
8
+ function isMarkdownFileName(name: string) {
9
+ return name.toLowerCase().endsWith(".md");
10
+ }
11
+
12
+ async function walkMarkdownFiles(root: string, maxFiles: number) {
13
+ const collected: string[] = [];
14
+ const queue = [root];
15
+ while (queue.length > 0 && collected.length < maxFiles) {
16
+ const current = queue.shift();
17
+ if (!current) {
18
+ continue;
19
+ }
20
+ const entries = await readdir(current, { withFileTypes: true });
21
+ for (const entry of entries) {
22
+ const absolutePath = join(current, entry.name);
23
+ if (entry.isDirectory()) {
24
+ queue.push(absolutePath);
25
+ continue;
26
+ }
27
+ if (entry.isFile() && isMarkdownFileName(entry.name)) {
28
+ collected.push(absolutePath);
29
+ if (collected.length >= maxFiles) {
30
+ break;
31
+ }
32
+ }
33
+ }
34
+ }
35
+ return collected.sort();
36
+ }
37
+
38
+ export async function listAgentOperatingMarkdownFiles(input: {
39
+ companyId: string;
40
+ agentId: string;
41
+ maxFiles?: number;
42
+ }) {
43
+ const root = resolveAgentOperatingPath(input.companyId, input.agentId);
44
+ await mkdir(root, { recursive: true });
45
+ const maxFiles = Math.max(1, Math.min(MAX_OBSERVABILITY_FILES, input.maxFiles ?? 100));
46
+ const files = await walkMarkdownFiles(root, maxFiles);
47
+ return files.map((filePath) => ({
48
+ path: filePath,
49
+ relativePath: relative(root, filePath),
50
+ operatingRoot: root
51
+ }));
52
+ }
53
+
54
+ export async function readAgentOperatingFile(input: {
55
+ companyId: string;
56
+ agentId: string;
57
+ relativePath: string;
58
+ }) {
59
+ const root = resolveAgentOperatingPath(input.companyId, input.agentId);
60
+ await mkdir(root, { recursive: true });
61
+ const candidate = resolve(root, input.relativePath);
62
+ if (!isInsidePath(root, candidate)) {
63
+ throw new Error("Requested operating path is outside of operating root.");
64
+ }
65
+ const info = await stat(candidate);
66
+ if (!info.isFile()) {
67
+ throw new Error("Requested operating path is not a file.");
68
+ }
69
+ if (info.size > MAX_OBSERVABILITY_FILE_BYTES) {
70
+ throw new Error("Requested operating file exceeds size limit.");
71
+ }
72
+ const content = await readFile(candidate, "utf8");
73
+ return {
74
+ path: candidate,
75
+ relativePath: relative(root, candidate),
76
+ content,
77
+ sizeBytes: info.size
78
+ };
79
+ }
80
+
81
+ export async function writeAgentOperatingFile(input: {
82
+ companyId: string;
83
+ agentId: string;
84
+ relativePath: string;
85
+ content: string;
86
+ }) {
87
+ const root = resolveAgentOperatingPath(input.companyId, input.agentId);
88
+ await mkdir(root, { recursive: true });
89
+ const normalizedRel = input.relativePath.trim();
90
+ if (!normalizedRel || normalizedRel.includes("..")) {
91
+ throw new Error("Invalid relative path.");
92
+ }
93
+ if (!isMarkdownFileName(normalizedRel)) {
94
+ throw new Error("Only .md files can be written under the operating directory.");
95
+ }
96
+ const candidate = resolve(root, normalizedRel);
97
+ if (!isInsidePath(root, candidate)) {
98
+ throw new Error("Requested operating path is outside of operating root.");
99
+ }
100
+ const bytes = Buffer.byteLength(input.content, "utf8");
101
+ if (bytes > MAX_OBSERVABILITY_FILE_BYTES) {
102
+ throw new Error("Content exceeds size limit.");
103
+ }
104
+ const parent = dirname(candidate);
105
+ if (!isInsidePath(root, parent)) {
106
+ throw new Error("Invalid parent directory.");
107
+ }
108
+ await mkdir(parent, { recursive: true });
109
+ await writeFile(candidate, input.content, { encoding: "utf8" });
110
+ const info = await stat(candidate);
111
+ return {
112
+ path: candidate,
113
+ relativePath: relative(root, candidate),
114
+ sizeBytes: info.size
115
+ };
116
+ }
@@ -60,19 +60,6 @@ const approvalGatedActions = new Set([
60
60
  const hireAgentPayloadSchema = AgentCreateRequestSchema.extend({
61
61
  sourceIssueId: z.string().min(1).optional(),
62
62
  sourceIssueIds: z.array(z.string().min(1)).default([]),
63
- delegationIntent: z
64
- .object({
65
- intentType: z.literal("agent_hiring_request"),
66
- requestedRole: z.string().nullable().optional(),
67
- requestedName: z.string().nullable().optional(),
68
- requestedManagerAgentId: z.string().nullable().optional(),
69
- requestedProviderType: z
70
- .enum(["claude_code", "codex", "cursor", "opencode", "gemini_cli", "openai_api", "anthropic_api", "http", "shell"])
71
- .nullable()
72
- .optional(),
73
- requestedRuntimeModel: z.string().nullable().optional()
74
- })
75
- .optional(),
76
63
  runtimeCommand: z.string().optional(),
77
64
  runtimeArgs: z.array(z.string()).optional(),
78
65
  runtimeCwd: z.string().optional(),
@@ -310,6 +297,7 @@ async function applyApprovalAction(db: BopoDb, companyId: string, action: string
310
297
  role: resolveAgentRoleText(parsed.data.role, parsed.data.roleKey, parsed.data.title),
311
298
  roleKey: normalizeRoleKey(parsed.data.roleKey),
312
299
  title: normalizeTitle(parsed.data.title),
300
+ capabilities: normalizeCapabilities(parsed.data.capabilities),
313
301
  name: parsed.data.name,
314
302
  providerType: parsed.data.providerType,
315
303
  heartbeatCron: parsed.data.heartbeatCron,
@@ -739,6 +727,11 @@ function normalizeTitle(input: string | null | undefined) {
739
727
  return normalized ? normalized : null;
740
728
  }
741
729
 
730
+ function normalizeCapabilities(input: string | null | undefined) {
731
+ const normalized = input?.trim();
732
+ return normalized ? normalized : null;
733
+ }
734
+
742
735
  function resolveAgentRoleText(
743
736
  legacyRole: string | undefined,
744
737
  roleKeyInput: string | undefined,
@@ -38,7 +38,7 @@ import {
38
38
  projects,
39
39
  sql
40
40
  } from "bopodev-db";
41
- import { appendAuditEvent, appendCost } from "bopodev-db";
41
+ import { appendAuditEvent, appendCost, listAgents } from "bopodev-db";
42
42
  import { parseRuntimeConfigFromAgentRow } from "../../lib/agent-config";
43
43
  import { bootstrapRepositoryWorkspace, ensureIsolatedGitWorktree, GitRuntimeError } from "../../lib/git-runtime";
44
44
  import {
@@ -669,7 +669,9 @@ export async function runHeartbeatForAgent(
669
669
  },
670
670
  failClosed: false
671
671
  });
672
- const isCommentOrderWake = options?.wakeContext?.reason === "issue_comment_recipient";
672
+ const isCommentOrderWake =
673
+ options?.wakeContext?.reason === "issue_comment_recipient" ||
674
+ options?.wakeContext?.reason === "loop_execution";
673
675
  const heartbeatIdlePolicy = resolveHeartbeatIdlePolicy();
674
676
  const workItems = isCommentOrderWake ? [] : await claimIssuesForAgent(db, companyId, agentId, runId);
675
677
  const wakeWorkItems = await loadWakeContextWorkItems(db, companyId, options?.wakeContext?.issueIds);
@@ -1941,7 +1943,10 @@ function resolveExecutionWorkItems(
1941
1943
  wakeContextItems: IssueWorkItemRow[],
1942
1944
  wakeContext?: HeartbeatWakeContext
1943
1945
  ) {
1944
- if (wakeContext?.reason === "issue_comment_recipient" && wakeContextItems.length > 0) {
1946
+ if (
1947
+ (wakeContext?.reason === "issue_comment_recipient" || wakeContext?.reason === "loop_execution") &&
1948
+ wakeContextItems.length > 0
1949
+ ) {
1945
1950
  return wakeContextItems;
1946
1951
  }
1947
1952
  return mergeContextWorkItems(assigned, wakeContextItems);
@@ -2182,6 +2187,22 @@ async function buildHeartbeatContext(
2182
2187
  const isCommentOrderWake = input.wakeContext?.reason === "issue_comment_recipient";
2183
2188
  const promptMode = resolveHeartbeatPromptMode();
2184
2189
 
2190
+ const companyAgentRows = await listAgents(db, companyId);
2191
+ const teamRoster = companyAgentRows
2192
+ .filter((row) => row.status !== "terminated")
2193
+ .sort((a, b) => {
2194
+ const byName = a.name.localeCompare(b.name);
2195
+ return byName !== 0 ? byName : a.id.localeCompare(b.id);
2196
+ })
2197
+ .map((row) => ({
2198
+ id: row.id,
2199
+ name: row.name,
2200
+ role: row.role,
2201
+ title: row.title ?? null,
2202
+ capabilities: row.capabilities ?? null,
2203
+ status: row.status
2204
+ }));
2205
+
2185
2206
  return {
2186
2207
  companyId,
2187
2208
  agentId: input.agentId,
@@ -2197,6 +2218,7 @@ async function buildHeartbeatContext(
2197
2218
  role: input.agentRole,
2198
2219
  managerAgentId: input.managerAgentId
2199
2220
  },
2221
+ teamRoster,
2200
2222
  state: input.state,
2201
2223
  memoryContext: input.memoryContext,
2202
2224
  runtime: input.runtime,
@@ -10,6 +10,7 @@ export type HeartbeatProviderType =
10
10
  | "gemini_cli"
11
11
  | "openai_api"
12
12
  | "anthropic_api"
13
+ | "openclaw_gateway"
13
14
  | "http"
14
15
  | "shell";
15
16
 
@@ -1,5 +1,5 @@
1
1
  import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
- import { join, relative, resolve } from "node:path";
2
+ import { dirname, join, relative, resolve } from "node:path";
3
3
  import type { AgentMemoryContext } from "bopodev-agent-sdk";
4
4
  import {
5
5
  isInsidePath,
@@ -227,6 +227,40 @@ export async function readAgentMemoryFile(input: {
227
227
  };
228
228
  }
229
229
 
230
+ export async function writeAgentMemoryFile(input: {
231
+ companyId: string;
232
+ agentId: string;
233
+ relativePath: string;
234
+ content: string;
235
+ }) {
236
+ const root = resolveAgentMemoryRootPath(input.companyId, input.agentId);
237
+ await mkdir(root, { recursive: true });
238
+ const normalizedRel = input.relativePath.trim();
239
+ if (!normalizedRel || normalizedRel.includes("..")) {
240
+ throw new Error("Invalid relative path.");
241
+ }
242
+ const candidate = resolve(root, normalizedRel);
243
+ if (!isInsidePath(root, candidate)) {
244
+ throw new Error("Requested memory path is outside of memory root.");
245
+ }
246
+ const bytes = Buffer.byteLength(input.content, "utf8");
247
+ if (bytes > MAX_OBSERVABILITY_FILE_BYTES) {
248
+ throw new Error("Content exceeds size limit.");
249
+ }
250
+ const parent = dirname(candidate);
251
+ if (!isInsidePath(root, parent)) {
252
+ throw new Error("Invalid parent directory.");
253
+ }
254
+ await mkdir(parent, { recursive: true });
255
+ await writeFile(candidate, input.content, { encoding: "utf8" });
256
+ const info = await stat(candidate);
257
+ return {
258
+ path: candidate,
259
+ relativePath: relative(root, candidate),
260
+ sizeBytes: info.size
261
+ };
262
+ }
263
+
230
264
  function collapseWhitespace(value: string) {
231
265
  return value.replace(/\s+/g, " ").trim();
232
266
  }
@@ -9,6 +9,7 @@ import {
9
9
  updatePluginConfig
10
10
  } from "bopodev-db";
11
11
  import { interpolateTemplateManifest, buildTemplatePreview } from "./template-preview-service";
12
+ import { addWorkLoopTrigger, createWorkLoop } from "./work-loop-service";
12
13
 
13
14
  export class TemplateApplyError extends Error {
14
15
  constructor(message: string) {
@@ -62,6 +63,7 @@ export async function applyTemplateManifest(
62
63
  role: resolveAgentRoleText(agent.role, agent.roleKey, agent.title),
63
64
  roleKey: normalizeRoleKey(agent.roleKey),
64
65
  title: normalizeTitle(agent.title),
66
+ capabilities: normalizeCapabilities(agent.capabilities),
65
67
  name: agent.name,
66
68
  providerType: agent.providerType,
67
69
  heartbeatCron: agent.heartbeatCron,
@@ -122,6 +124,38 @@ export async function applyTemplateManifest(
122
124
  });
123
125
  }
124
126
 
127
+ const firstProjectId =
128
+ renderedManifest.projects.length > 0
129
+ ? projectIdByKey.get(renderedManifest.projects[0]!.key) ?? null
130
+ : Array.from(projectIdByKey.values())[0] ?? null;
131
+ for (const job of renderedManifest.recurrence) {
132
+ if (job.targetType !== "agent") {
133
+ continue;
134
+ }
135
+ const assigneeAgentId = agentIdByKey.get(job.targetKey) ?? null;
136
+ if (!assigneeAgentId || !firstProjectId) {
137
+ continue;
138
+ }
139
+ const title =
140
+ job.instruction?.trim() && job.instruction.trim().length > 0
141
+ ? job.instruction.trim()
142
+ : `Recurring work: ${job.targetKey}`;
143
+ const loop = await createWorkLoop(db, {
144
+ companyId: input.companyId,
145
+ projectId: firstProjectId,
146
+ title,
147
+ description: job.instruction?.trim() || null,
148
+ assigneeAgentId
149
+ });
150
+ if (loop) {
151
+ await addWorkLoopTrigger(db, {
152
+ companyId: input.companyId,
153
+ workLoopId: loop.id,
154
+ cronExpression: job.cron
155
+ });
156
+ }
157
+ }
158
+
125
159
  const install = await createTemplateInstall(db, {
126
160
  companyId: input.companyId,
127
161
  templateId: input.templateId,
@@ -152,6 +186,11 @@ function normalizeTitle(input: string | null | undefined) {
152
186
  return normalized ? normalized : null;
153
187
  }
154
188
 
189
+ function normalizeCapabilities(input: string | null | undefined) {
190
+ const normalized = input?.trim();
191
+ return normalized ? normalized : null;
192
+ }
193
+
155
194
  function resolveAgentRoleText(
156
195
  legacyRole: string | undefined,
157
196
  roleKeyInput: string | undefined,
@@ -9,7 +9,8 @@ import {
9
9
  createTemplate,
10
10
  createTemplateVersion,
11
11
  getTemplateBySlug,
12
- getTemplateVersionByVersion
12
+ getTemplateVersionByVersion,
13
+ updateTemplate
13
14
  } from "bopodev-db";
14
15
 
15
16
  type BuiltinTemplateDefinition = {
@@ -28,7 +29,7 @@ const builtinTemplateDefinitions: BuiltinTemplateDefinition[] = [
28
29
  slug: "founder-startup-basic",
29
30
  name: "Founder Startup Basic",
30
31
  description: "Baseline operating company for solo founders launching and shipping with AI agents.",
31
- version: "1.0.0",
32
+ version: "1.0.1",
32
33
  status: "published",
33
34
  visibility: "company",
34
35
  variables: [
@@ -78,6 +79,10 @@ const builtinTemplateDefinitions: BuiltinTemplateDefinition[] = [
78
79
  {
79
80
  key: "founder-ceo",
80
81
  role: "CEO",
82
+ roleKey: "ceo",
83
+ title: "Founder CEO",
84
+ capabilities:
85
+ "Sets company priorities, runs leadership cadence, hires and coordinates agents toward mission outcomes.",
81
86
  name: "Founder CEO",
82
87
  providerType: "codex",
83
88
  heartbeatCron: "*/15 * * * *",
@@ -119,6 +124,10 @@ const builtinTemplateDefinitions: BuiltinTemplateDefinition[] = [
119
124
  {
120
125
  key: "founding-engineer",
121
126
  role: "Founding Engineer",
127
+ roleKey: "engineer",
128
+ title: "Founding Engineer",
129
+ capabilities:
130
+ "Ships product improvements with small reviewable changes, tests, and clear handoffs to stakeholders.",
122
131
  name: "Founding Engineer",
123
132
  managerAgentKey: "founder-ceo",
124
133
  providerType: "codex",
@@ -152,6 +161,10 @@ const builtinTemplateDefinitions: BuiltinTemplateDefinition[] = [
152
161
  {
153
162
  key: "growth-operator",
154
163
  role: "Growth Operator",
164
+ roleKey: "general",
165
+ title: "Growth Operator",
166
+ capabilities:
167
+ "Runs growth experiments, measures funnel impact, and feeds learnings back to leadership with clear next steps.",
155
168
  name: "Growth Operator",
156
169
  managerAgentKey: "founder-ceo",
157
170
  providerType: "codex",
@@ -225,7 +238,7 @@ const builtinTemplateDefinitions: BuiltinTemplateDefinition[] = [
225
238
  slug: "marketing-content-engine",
226
239
  name: "Marketing Content Engine",
227
240
  description: "Content marketing operating template for publishing, distribution, and analytics loops.",
228
- version: "1.0.0",
241
+ version: "1.0.1",
229
242
  status: "published",
230
243
  visibility: "company",
231
244
  variables: [
@@ -276,6 +289,10 @@ const builtinTemplateDefinitions: BuiltinTemplateDefinition[] = [
276
289
  {
277
290
  key: "head-of-marketing",
278
291
  role: "Head of Marketing",
292
+ roleKey: "cmo",
293
+ title: "Head of Marketing",
294
+ capabilities:
295
+ "Owns marketing narrative, cross-functional alignment, and weekly performance decisions for pipeline growth.",
279
296
  name: "Head of Marketing",
280
297
  providerType: "codex",
281
298
  heartbeatCron: "*/20 * * * *",
@@ -308,6 +325,10 @@ const builtinTemplateDefinitions: BuiltinTemplateDefinition[] = [
308
325
  {
309
326
  key: "content-strategist",
310
327
  role: "Content Strategist",
328
+ roleKey: "general",
329
+ title: "Content Strategist",
330
+ capabilities:
331
+ "Builds editorial calendars, briefs, and topic architecture tied to audience segments and revenue goals.",
311
332
  name: "Content Strategist",
312
333
  managerAgentKey: "head-of-marketing",
313
334
  providerType: "codex",
@@ -337,6 +358,10 @@ const builtinTemplateDefinitions: BuiltinTemplateDefinition[] = [
337
358
  {
338
359
  key: "content-writer",
339
360
  role: "Content Writer",
361
+ roleKey: "general",
362
+ title: "Content Writer",
363
+ capabilities:
364
+ "Produces channel-ready drafts, headline and CTA options, and repurposing notes aligned to campaign intent.",
340
365
  name: "Content Writer",
341
366
  managerAgentKey: "head-of-marketing",
342
367
  providerType: "codex",
@@ -367,6 +392,10 @@ const builtinTemplateDefinitions: BuiltinTemplateDefinition[] = [
367
392
  {
368
393
  key: "distribution-manager",
369
394
  role: "Distribution Manager",
395
+ roleKey: "general",
396
+ title: "Distribution Manager",
397
+ capabilities:
398
+ "Distributes and repurposes assets across channels with tracking discipline and weekly performance reporting.",
370
399
  name: "Distribution Manager",
371
400
  managerAgentKey: "head-of-marketing",
372
401
  providerType: "codex",
@@ -482,6 +511,11 @@ export async function ensureCompanyBuiltinTemplateDefaults(db: BopoDb, companyId
482
511
  version: definition.version,
483
512
  manifestJson: JSON.stringify(manifest)
484
513
  });
514
+ await updateTemplate(db, {
515
+ companyId,
516
+ id: template.id,
517
+ currentVersion: definition.version
518
+ });
485
519
  }
486
520
  }
487
521
  }
@@ -0,0 +1,2 @@
1
+ export * from "./loop-cron";
2
+ export * from "./work-loop-service";