bopodev-api 0.1.24 → 0.1.26
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 +4 -4
- package/src/app.ts +11 -1
- package/src/http.ts +39 -0
- package/src/lib/comment-recipients.ts +105 -0
- package/src/lib/hiring-delegate.ts +7 -6
- package/src/lib/instance-paths.ts +11 -0
- package/src/realtime/attention.ts +47 -0
- package/src/realtime/governance.ts +11 -3
- package/src/realtime/heartbeat-runs.ts +33 -11
- package/src/realtime/hub.ts +34 -2
- package/src/realtime/office-space.ts +17 -1
- package/src/routes/agents.ts +81 -12
- package/src/routes/attention.ts +112 -0
- package/src/routes/companies.ts +13 -5
- package/src/routes/goals.ts +10 -2
- package/src/routes/governance.ts +85 -2
- package/src/routes/heartbeats.ts +81 -43
- package/src/routes/issues.ts +293 -62
- package/src/routes/observability.ts +219 -10
- package/src/routes/projects.ts +7 -2
- package/src/scripts/onboard-seed.ts +8 -7
- package/src/server.ts +3 -1
- package/src/services/attention-service.ts +412 -0
- package/src/services/budget-service.ts +99 -2
- package/src/services/comment-recipient-dispatch-service.ts +158 -0
- package/src/services/governance-service.ts +237 -14
- package/src/services/heartbeat-queue-service.ts +318 -0
- package/src/services/heartbeat-service.ts +2341 -278
- package/src/services/memory-file-service.ts +510 -35
- package/src/services/plugin-runtime.ts +33 -1
- package/src/services/template-apply-service.ts +37 -2
- package/src/worker/scheduler.ts +46 -8
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, isAbsolute, resolve } from "node:path";
|
|
2
4
|
import { z } from "zod";
|
|
3
5
|
import {
|
|
4
6
|
getHeartbeatRun,
|
|
7
|
+
listCompanies,
|
|
5
8
|
listAgents,
|
|
6
9
|
listAuditEvents,
|
|
7
10
|
listCostEntries,
|
|
11
|
+
listGoals,
|
|
8
12
|
listHeartbeatRunMessages,
|
|
9
13
|
listHeartbeatRuns,
|
|
10
14
|
listModelPricing,
|
|
@@ -13,9 +17,10 @@ import {
|
|
|
13
17
|
} from "bopodev-db";
|
|
14
18
|
import type { AppContext } from "../context";
|
|
15
19
|
import { sendError, sendOk } from "../http";
|
|
20
|
+
import { isInsidePath, resolveCompanyWorkspaceRootPath } from "../lib/instance-paths";
|
|
16
21
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
17
22
|
import { requirePermission } from "../middleware/request-actor";
|
|
18
|
-
import { listAgentMemoryFiles, readAgentMemoryFile } from "../services/memory-file-service";
|
|
23
|
+
import { listAgentMemoryFiles, loadAgentMemoryContext, readAgentMemoryFile } from "../services/memory-file-service";
|
|
19
24
|
|
|
20
25
|
export function createObservabilityRouter(ctx: AppContext) {
|
|
21
26
|
const router = Router();
|
|
@@ -111,10 +116,12 @@ export function createObservabilityRouter(ctx: AppContext) {
|
|
|
111
116
|
.filter((run) => (agentFilter ? run.agentId === agentFilter : true))
|
|
112
117
|
.map((run) => {
|
|
113
118
|
const details = runDetailsByRunId.get(run.id);
|
|
114
|
-
const
|
|
119
|
+
const report = toRecord(details?.report);
|
|
120
|
+
const outcome = details?.outcome ?? report?.outcome ?? null;
|
|
115
121
|
return {
|
|
116
|
-
...serializeRunRow(run,
|
|
117
|
-
outcome
|
|
122
|
+
...serializeRunRow(run, details),
|
|
123
|
+
outcome,
|
|
124
|
+
report: report ?? null
|
|
118
125
|
};
|
|
119
126
|
})
|
|
120
127
|
);
|
|
@@ -136,7 +143,7 @@ export function createObservabilityRouter(ctx: AppContext) {
|
|
|
136
143
|
const trace = toRecord(details?.trace);
|
|
137
144
|
const traceTranscript = Array.isArray(trace?.transcript) ? trace.transcript : [];
|
|
138
145
|
return sendOk(res, {
|
|
139
|
-
run: serializeRunRow(run, details
|
|
146
|
+
run: serializeRunRow(run, details),
|
|
140
147
|
details,
|
|
141
148
|
transcript: {
|
|
142
149
|
hasPersistedMessages: transcriptResult.items.length > 0,
|
|
@@ -146,6 +153,46 @@ export function createObservabilityRouter(ctx: AppContext) {
|
|
|
146
153
|
});
|
|
147
154
|
});
|
|
148
155
|
|
|
156
|
+
router.get("/heartbeats/:runId/artifacts/:artifactIndex/download", async (req, res) => {
|
|
157
|
+
const companyId = req.companyId!;
|
|
158
|
+
const runId = req.params.runId;
|
|
159
|
+
const rawArtifactIndex = Number(req.params.artifactIndex);
|
|
160
|
+
const artifactIndex = Number.isFinite(rawArtifactIndex) ? Math.floor(rawArtifactIndex) : NaN;
|
|
161
|
+
if (!Number.isInteger(artifactIndex) || artifactIndex < 0) {
|
|
162
|
+
return sendError(res, "Artifact index must be a non-negative integer.", 422);
|
|
163
|
+
}
|
|
164
|
+
const [run, auditRows] = await Promise.all([getHeartbeatRun(ctx.db, companyId, runId), listAuditEvents(ctx.db, companyId, 500)]);
|
|
165
|
+
if (!run) {
|
|
166
|
+
return sendError(res, "Run not found", 404);
|
|
167
|
+
}
|
|
168
|
+
const details = buildRunDetailsMap(auditRows).get(runId) ?? null;
|
|
169
|
+
const report = toRecord(details?.report);
|
|
170
|
+
const artifacts = Array.isArray(report?.artifacts)
|
|
171
|
+
? report.artifacts.filter((entry) => typeof entry === "object" && entry !== null)
|
|
172
|
+
: [];
|
|
173
|
+
const artifact = (artifacts[artifactIndex] ?? null) as Record<string, unknown> | null;
|
|
174
|
+
if (!artifact) {
|
|
175
|
+
return sendError(res, "Artifact not found.", 404);
|
|
176
|
+
}
|
|
177
|
+
const resolvedPath = resolveRunArtifactAbsolutePath(companyId, artifact);
|
|
178
|
+
if (!resolvedPath) {
|
|
179
|
+
return sendError(res, "Artifact path is invalid.", 422);
|
|
180
|
+
}
|
|
181
|
+
let stats: Awaited<ReturnType<typeof stat>>;
|
|
182
|
+
try {
|
|
183
|
+
stats = await stat(resolvedPath);
|
|
184
|
+
} catch {
|
|
185
|
+
return sendError(res, "Artifact not found on disk.", 404);
|
|
186
|
+
}
|
|
187
|
+
if (!stats.isFile()) {
|
|
188
|
+
return sendError(res, "Artifact is not a file.", 422);
|
|
189
|
+
}
|
|
190
|
+
const buffer = await readFile(resolvedPath);
|
|
191
|
+
res.setHeader("content-type", "application/octet-stream");
|
|
192
|
+
res.setHeader("content-disposition", `inline; filename="${encodeURIComponent(basename(resolvedPath))}"`);
|
|
193
|
+
return res.send(buffer);
|
|
194
|
+
});
|
|
195
|
+
|
|
149
196
|
router.get("/heartbeats/:runId/messages", async (req, res) => {
|
|
150
197
|
const companyId = req.companyId!;
|
|
151
198
|
const runId = req.params.runId;
|
|
@@ -266,6 +313,65 @@ export function createObservabilityRouter(ctx: AppContext) {
|
|
|
266
313
|
}
|
|
267
314
|
});
|
|
268
315
|
|
|
316
|
+
router.get("/memory/:agentId/context-preview", async (req, res) => {
|
|
317
|
+
const companyId = req.companyId!;
|
|
318
|
+
const agentId = req.params.agentId;
|
|
319
|
+
const projectIds = typeof req.query.projectIds === "string"
|
|
320
|
+
? req.query.projectIds
|
|
321
|
+
.split(",")
|
|
322
|
+
.map((entry) => entry.trim())
|
|
323
|
+
.filter(Boolean)
|
|
324
|
+
: [];
|
|
325
|
+
const queryText = typeof req.query.query === "string" ? req.query.query.trim() : "";
|
|
326
|
+
const [agents, goals, companies] = await Promise.all([
|
|
327
|
+
listAgents(ctx.db, companyId),
|
|
328
|
+
listGoals(ctx.db, companyId),
|
|
329
|
+
listCompanies(ctx.db)
|
|
330
|
+
]);
|
|
331
|
+
const agent = agents.find((entry) => entry.id === agentId);
|
|
332
|
+
if (!agent) {
|
|
333
|
+
return sendError(res, "Agent not found", 404);
|
|
334
|
+
}
|
|
335
|
+
const company = companies.find((entry) => entry.id === companyId);
|
|
336
|
+
const memoryContext = await loadAgentMemoryContext({
|
|
337
|
+
companyId,
|
|
338
|
+
agentId,
|
|
339
|
+
projectIds,
|
|
340
|
+
queryText: queryText.length > 0 ? queryText : undefined
|
|
341
|
+
});
|
|
342
|
+
const activeCompanyGoals = goals
|
|
343
|
+
.filter((goal) => goal.status === "active" && goal.level === "company")
|
|
344
|
+
.map((goal) => goal.title);
|
|
345
|
+
const activeProjectGoals = goals
|
|
346
|
+
.filter((goal) => goal.status === "active" && goal.level === "project" && goal.projectId && projectIds.includes(goal.projectId))
|
|
347
|
+
.map((goal) => goal.title);
|
|
348
|
+
const activeAgentGoals = goals
|
|
349
|
+
.filter((goal) => goal.status === "active" && goal.level === "agent")
|
|
350
|
+
.map((goal) => goal.title);
|
|
351
|
+
const compiledPreview = [
|
|
352
|
+
`Agent: ${agent.name} (${agent.role})`,
|
|
353
|
+
`Company mission: ${company?.mission ?? "No mission set"}`,
|
|
354
|
+
`Company goals: ${activeCompanyGoals.length > 0 ? activeCompanyGoals.join(" | ") : "None"}`,
|
|
355
|
+
`Project goals: ${activeProjectGoals.length > 0 ? activeProjectGoals.join(" | ") : "None"}`,
|
|
356
|
+
`Agent goals: ${activeAgentGoals.length > 0 ? activeAgentGoals.join(" | ") : "None"}`,
|
|
357
|
+
`Tacit notes: ${memoryContext.tacitNotes ?? "None"}`,
|
|
358
|
+
`Durable facts: ${memoryContext.durableFacts.join(" | ") || "None"}`,
|
|
359
|
+
`Daily notes: ${memoryContext.dailyNotes.join(" | ") || "None"}`
|
|
360
|
+
].join("\n");
|
|
361
|
+
return sendOk(res, {
|
|
362
|
+
agentId,
|
|
363
|
+
projectIds,
|
|
364
|
+
companyMission: company?.mission ?? null,
|
|
365
|
+
goalContext: {
|
|
366
|
+
companyGoals: activeCompanyGoals,
|
|
367
|
+
projectGoals: activeProjectGoals,
|
|
368
|
+
agentGoals: activeAgentGoals
|
|
369
|
+
},
|
|
370
|
+
memoryContext,
|
|
371
|
+
compiledPreview
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
269
375
|
router.get("/plugins/runs", async (req, res) => {
|
|
270
376
|
const companyId = req.companyId!;
|
|
271
377
|
const pluginId = typeof req.query.pluginId === "string" && req.query.pluginId.trim() ? req.query.pluginId.trim() : undefined;
|
|
@@ -298,6 +404,85 @@ function toRecord(value: unknown) {
|
|
|
298
404
|
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
|
|
299
405
|
}
|
|
300
406
|
|
|
407
|
+
function resolveRunArtifactAbsolutePath(companyId: string, artifact: Record<string, unknown>) {
|
|
408
|
+
const companyWorkspaceRoot = resolveCompanyWorkspaceRootPath(companyId);
|
|
409
|
+
const absolutePathRaw = normalizeAbsoluteArtifactPath(
|
|
410
|
+
typeof artifact.absolutePath === "string" ? artifact.absolutePath.trim() : ""
|
|
411
|
+
);
|
|
412
|
+
const relativePathRaw = normalizeWorkspaceRelativeArtifactPath(
|
|
413
|
+
typeof artifact.relativePath === "string"
|
|
414
|
+
? artifact.relativePath.trim()
|
|
415
|
+
: typeof artifact.path === "string"
|
|
416
|
+
? artifact.path.trim()
|
|
417
|
+
: "",
|
|
418
|
+
companyId
|
|
419
|
+
);
|
|
420
|
+
const candidate = relativePathRaw
|
|
421
|
+
? resolve(companyWorkspaceRoot, relativePathRaw)
|
|
422
|
+
: absolutePathRaw
|
|
423
|
+
? absolutePathRaw
|
|
424
|
+
: "";
|
|
425
|
+
if (!candidate) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(companyWorkspaceRoot, candidate);
|
|
429
|
+
if (!isInsidePath(companyWorkspaceRoot, resolved)) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
return resolved;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function normalizeAbsoluteArtifactPath(value: string) {
|
|
436
|
+
const trimmed = value.trim();
|
|
437
|
+
if (!trimmed || !isAbsolute(trimmed)) {
|
|
438
|
+
return "";
|
|
439
|
+
}
|
|
440
|
+
return resolve(trimmed);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function normalizeWorkspaceRelativeArtifactPath(value: string, companyId: string) {
|
|
444
|
+
const trimmed = value.trim();
|
|
445
|
+
if (!trimmed) {
|
|
446
|
+
return "";
|
|
447
|
+
}
|
|
448
|
+
const unixSeparated = trimmed.replace(/\\/g, "/");
|
|
449
|
+
if (isAbsolute(unixSeparated)) {
|
|
450
|
+
return "";
|
|
451
|
+
}
|
|
452
|
+
const parts: string[] = [];
|
|
453
|
+
for (const part of unixSeparated.split("/")) {
|
|
454
|
+
if (!part || part === ".") {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (part === "..") {
|
|
458
|
+
if (parts.length > 0 && parts[parts.length - 1] !== "..") {
|
|
459
|
+
parts.pop();
|
|
460
|
+
} else {
|
|
461
|
+
parts.push(part);
|
|
462
|
+
}
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
parts.push(part);
|
|
466
|
+
}
|
|
467
|
+
const normalized = parts.join("/");
|
|
468
|
+
if (!normalized) {
|
|
469
|
+
return "";
|
|
470
|
+
}
|
|
471
|
+
const workspaceScopedMatch = normalized.match(/(?:^|\/)workspace\/([^/]+)\/(.+)$/);
|
|
472
|
+
if (!workspaceScopedMatch) {
|
|
473
|
+
return normalized;
|
|
474
|
+
}
|
|
475
|
+
const scopedCompanyId = workspaceScopedMatch[1];
|
|
476
|
+
const scopedRelativePath = workspaceScopedMatch[2];
|
|
477
|
+
if (!scopedCompanyId || !scopedRelativePath) {
|
|
478
|
+
return "";
|
|
479
|
+
}
|
|
480
|
+
if (scopedCompanyId !== companyId) {
|
|
481
|
+
return "";
|
|
482
|
+
}
|
|
483
|
+
return scopedRelativePath;
|
|
484
|
+
}
|
|
485
|
+
|
|
301
486
|
function serializeRunRow(
|
|
302
487
|
run: {
|
|
303
488
|
id: string;
|
|
@@ -308,14 +493,27 @@ function serializeRunRow(
|
|
|
308
493
|
finishedAt: Date | null;
|
|
309
494
|
message: string | null;
|
|
310
495
|
},
|
|
311
|
-
|
|
496
|
+
details: Record<string, unknown> | null | undefined
|
|
312
497
|
) {
|
|
313
|
-
const runType = resolveRunType(run,
|
|
498
|
+
const runType = resolveRunType(run, details);
|
|
499
|
+
const report = toRecord(details?.report);
|
|
500
|
+
const publicStatusRaw = typeof report?.finalStatus === "string" ? report.finalStatus : null;
|
|
501
|
+
const publicStatus =
|
|
502
|
+
publicStatusRaw === "completed" || publicStatusRaw === "failed"
|
|
503
|
+
? publicStatusRaw
|
|
504
|
+
: run.status === "started"
|
|
505
|
+
? "started"
|
|
506
|
+
: run.status === "failed"
|
|
507
|
+
? "failed"
|
|
508
|
+
: run.status === "completed"
|
|
509
|
+
? "completed"
|
|
510
|
+
: "failed";
|
|
314
511
|
return {
|
|
315
512
|
id: run.id,
|
|
316
513
|
companyId: run.companyId,
|
|
317
514
|
agentId: run.agentId,
|
|
318
515
|
status: run.status,
|
|
516
|
+
publicStatus,
|
|
319
517
|
startedAt: run.startedAt.toISOString(),
|
|
320
518
|
finishedAt: run.finishedAt?.toISOString() ?? null,
|
|
321
519
|
message: run.message ?? null,
|
|
@@ -328,14 +526,25 @@ function resolveRunType(
|
|
|
328
526
|
status: string;
|
|
329
527
|
message: string | null;
|
|
330
528
|
},
|
|
331
|
-
|
|
529
|
+
details: Record<string, unknown> | null | undefined
|
|
332
530
|
): "work" | "no_assigned_work" | "budget_skip" | "overlap_skip" | "other_skip" | "failed" | "running" {
|
|
333
531
|
if (run.status === "started") {
|
|
334
532
|
return "running";
|
|
335
533
|
}
|
|
336
|
-
|
|
534
|
+
const report = toRecord(details?.report);
|
|
535
|
+
const completionReason = typeof report?.completionReason === "string" ? report.completionReason : null;
|
|
536
|
+
if (run.status === "failed" || completionReason === "runtime_error" || completionReason === "provider_unavailable") {
|
|
337
537
|
return "failed";
|
|
338
538
|
}
|
|
539
|
+
if (completionReason === "no_assigned_work") {
|
|
540
|
+
return "no_assigned_work";
|
|
541
|
+
}
|
|
542
|
+
if (completionReason === "budget_hard_stop") {
|
|
543
|
+
return "budget_skip";
|
|
544
|
+
}
|
|
545
|
+
if (completionReason === "overlap_in_progress") {
|
|
546
|
+
return "overlap_skip";
|
|
547
|
+
}
|
|
339
548
|
const normalizedMessage = (run.message ?? "").toLowerCase();
|
|
340
549
|
if (normalizedMessage.includes("already in progress")) {
|
|
341
550
|
return "overlap_skip";
|
|
@@ -346,7 +555,7 @@ function resolveRunType(
|
|
|
346
555
|
if (isNoAssignedWorkMessage(run.message)) {
|
|
347
556
|
return "no_assigned_work";
|
|
348
557
|
}
|
|
349
|
-
if (isNoAssignedWorkOutcome(outcome)) {
|
|
558
|
+
if (isNoAssignedWorkOutcome(details?.outcome)) {
|
|
350
559
|
return "no_assigned_work";
|
|
351
560
|
}
|
|
352
561
|
if (run.status === "skipped") {
|
package/src/routes/projects.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Router } from "express";
|
|
|
2
2
|
import { mkdir, stat } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { ProjectSchema } from "bopodev-contracts";
|
|
5
6
|
import {
|
|
6
7
|
appendAuditEvent,
|
|
7
8
|
createProject,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
updateProjectWorkspace
|
|
16
17
|
} from "bopodev-db";
|
|
17
18
|
import type { AppContext } from "../context";
|
|
18
|
-
import { sendError, sendOk } from "../http";
|
|
19
|
+
import { sendError, sendOk, sendOkValidated } from "../http";
|
|
19
20
|
import { normalizeCompanyWorkspacePath, resolveProjectWorkspacePath } from "../lib/instance-paths";
|
|
20
21
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
21
22
|
import { requirePermission } from "../middleware/request-actor";
|
|
@@ -50,6 +51,7 @@ const createProjectSchema = z.object({
|
|
|
50
51
|
description: z.string().optional(),
|
|
51
52
|
status: projectStatusSchema.default("planned"),
|
|
52
53
|
plannedStartAt: z.string().optional(),
|
|
54
|
+
monthlyBudgetUsd: z.number().positive().default(100),
|
|
53
55
|
executionWorkspacePolicy: executionWorkspacePolicySchema.optional().nullable(),
|
|
54
56
|
workspace: z
|
|
55
57
|
.object({
|
|
@@ -69,6 +71,7 @@ const updateProjectSchema = z
|
|
|
69
71
|
description: z.string().nullable().optional(),
|
|
70
72
|
status: projectStatusSchema.optional(),
|
|
71
73
|
plannedStartAt: z.string().nullable().optional(),
|
|
74
|
+
monthlyBudgetUsd: z.number().positive().optional(),
|
|
72
75
|
executionWorkspacePolicy: executionWorkspacePolicySchema.nullable().optional(),
|
|
73
76
|
goalIds: z.array(z.string().min(1)).optional()
|
|
74
77
|
})
|
|
@@ -122,7 +125,7 @@ export function createProjectsRouter(ctx: AppContext) {
|
|
|
122
125
|
router.get("/", async (req, res) => {
|
|
123
126
|
const projects = await listProjects(ctx.db, req.companyId!);
|
|
124
127
|
const withDiagnostics = await Promise.all(projects.map((project) => enrichProjectDiagnostics(req.companyId!, project)));
|
|
125
|
-
return
|
|
128
|
+
return sendOkValidated(res, ProjectSchema.array(), withDiagnostics, "projects.list");
|
|
126
129
|
});
|
|
127
130
|
|
|
128
131
|
router.post("/", async (req, res) => {
|
|
@@ -140,6 +143,7 @@ export function createProjectsRouter(ctx: AppContext) {
|
|
|
140
143
|
description: parsed.data.description,
|
|
141
144
|
status: parsed.data.status,
|
|
142
145
|
plannedStartAt: parsePlannedStartAt(parsed.data.plannedStartAt),
|
|
146
|
+
monthlyBudgetUsd: parsed.data.monthlyBudgetUsd.toFixed(4),
|
|
143
147
|
executionWorkspacePolicy: parsed.data.executionWorkspacePolicy ?? null
|
|
144
148
|
});
|
|
145
149
|
if (!project) {
|
|
@@ -216,6 +220,7 @@ export function createProjectsRouter(ctx: AppContext) {
|
|
|
216
220
|
status: parsed.data.status,
|
|
217
221
|
plannedStartAt:
|
|
218
222
|
parsed.data.plannedStartAt === undefined ? undefined : parsePlannedStartAt(parsed.data.plannedStartAt),
|
|
223
|
+
monthlyBudgetUsd: parsed.data.monthlyBudgetUsd === undefined ? undefined : parsed.data.monthlyBudgetUsd.toFixed(4),
|
|
219
224
|
executionWorkspacePolicy: parsed.data.executionWorkspacePolicy
|
|
220
225
|
});
|
|
221
226
|
if (!project) {
|
|
@@ -26,7 +26,6 @@ import { normalizeRuntimeConfig, runtimeConfigToDb, runtimeConfigToStateBlobPatc
|
|
|
26
26
|
import {
|
|
27
27
|
normalizeAbsolutePath,
|
|
28
28
|
normalizeCompanyWorkspacePath,
|
|
29
|
-
resolveAgentFallbackWorkspacePath,
|
|
30
29
|
resolveProjectWorkspacePath
|
|
31
30
|
} from "../lib/instance-paths";
|
|
32
31
|
import { buildDefaultCeoBootstrapPrompt } from "../lib/ceo-bootstrap-prompt";
|
|
@@ -101,7 +100,7 @@ export async function ensureOnboardingSeed(input: {
|
|
|
101
100
|
const resolvedCompanyName = companyRow.name;
|
|
102
101
|
await ensureCompanyBuiltinTemplateDefaults(db, companyId);
|
|
103
102
|
const agents = await listAgents(db, companyId);
|
|
104
|
-
const existingCeo = agents.find((agent) => agent.role === "CEO" || agent.name === "CEO");
|
|
103
|
+
const existingCeo = agents.find((agent) => agent.roleKey === "ceo" || agent.role === "CEO" || agent.name === "CEO");
|
|
105
104
|
let ceoCreated = false;
|
|
106
105
|
let ceoMigrated = false;
|
|
107
106
|
let ceoProviderType: AgentProvider = parseAgentProvider(existingCeo?.providerType) ?? agentProvider;
|
|
@@ -130,6 +129,8 @@ export async function ensureOnboardingSeed(input: {
|
|
|
130
129
|
const ceo = await createAgent(db, {
|
|
131
130
|
companyId,
|
|
132
131
|
role: "CEO",
|
|
132
|
+
roleKey: "ceo",
|
|
133
|
+
title: "CEO",
|
|
133
134
|
name: "CEO",
|
|
134
135
|
providerType: agentProvider,
|
|
135
136
|
heartbeatCron: "*/5 * * * *",
|
|
@@ -303,15 +304,15 @@ async function ensureCeoStartupTask(
|
|
|
303
304
|
typeof issue.body === "string" &&
|
|
304
305
|
issue.body.includes(CEO_STARTUP_TASK_MARKER)
|
|
305
306
|
);
|
|
306
|
-
const
|
|
307
|
-
const ceoOperatingFolder = `${
|
|
308
|
-
const ceoTmpFolder = `${
|
|
307
|
+
const companyScopedCeoRoot = `workspace/${input.companyId}/agents/${input.ceoId}`;
|
|
308
|
+
const ceoOperatingFolder = `${companyScopedCeoRoot}/operating`;
|
|
309
|
+
const ceoTmpFolder = `${companyScopedCeoRoot}/tmp`;
|
|
309
310
|
const body = [
|
|
310
311
|
CEO_STARTUP_TASK_MARKER,
|
|
311
312
|
"",
|
|
312
313
|
"Stand up your leadership operating baseline before taking on additional delivery work.",
|
|
313
314
|
"",
|
|
314
|
-
`1. Create your operating folder at \`${ceoOperatingFolder}
|
|
315
|
+
`1. Create your operating folder at \`${ceoOperatingFolder}/\`.`,
|
|
315
316
|
"2. Author these files with your own voice and responsibilities:",
|
|
316
317
|
` - \`${ceoOperatingFolder}/AGENTS.md\``,
|
|
317
318
|
` - \`${ceoOperatingFolder}/HEARTBEAT.md\``,
|
|
@@ -333,7 +334,7 @@ async function ensureCeoStartupTask(
|
|
|
333
334
|
"7. Do not use unsupported hire fields such as `adapterType`, `adapterConfig`, or `reportsTo`.",
|
|
334
335
|
"",
|
|
335
336
|
"Safety checks before requesting hire:",
|
|
336
|
-
|
|
337
|
+
`- Keep operating/system files inside \`workspace/${input.companyId}/agents/${input.ceoId}/\` only.`,
|
|
337
338
|
"- Do not request duplicates if a Founding Engineer already exists.",
|
|
338
339
|
"- Do not request duplicates if a pending approval for the same role is already open.",
|
|
339
340
|
"- For control-plane calls, prefer direct header env vars (`BOPODEV_COMPANY_ID`, `BOPODEV_ACTOR_TYPE`, `BOPODEV_ACTOR_ID`, `BOPODEV_ACTOR_COMPANIES`, `BOPODEV_ACTOR_PERMISSIONS`) instead of parsing `BOPODEV_REQUEST_HEADERS_JSON`.",
|
package/src/server.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { createApp } from "./app";
|
|
|
10
10
|
import { loadGovernanceRealtimeSnapshot } from "./realtime/governance";
|
|
11
11
|
import { loadOfficeSpaceRealtimeSnapshot } from "./realtime/office-space";
|
|
12
12
|
import { loadHeartbeatRunsRealtimeSnapshot } from "./realtime/heartbeat-runs";
|
|
13
|
+
import { loadAttentionRealtimeSnapshot } from "./realtime/attention";
|
|
13
14
|
import { attachRealtimeHub } from "./realtime/hub";
|
|
14
15
|
import {
|
|
15
16
|
isAuthenticatedMode,
|
|
@@ -106,7 +107,8 @@ async function main() {
|
|
|
106
107
|
bootstrapLoaders: {
|
|
107
108
|
governance: (companyId) => loadGovernanceRealtimeSnapshot(db, companyId),
|
|
108
109
|
"office-space": (companyId) => loadOfficeSpaceRealtimeSnapshot(db, companyId),
|
|
109
|
-
"heartbeat-runs": (companyId) => loadHeartbeatRunsRealtimeSnapshot(db, companyId)
|
|
110
|
+
"heartbeat-runs": (companyId) => loadHeartbeatRunsRealtimeSnapshot(db, companyId),
|
|
111
|
+
attention: (companyId) => loadAttentionRealtimeSnapshot(db, companyId)
|
|
110
112
|
}
|
|
111
113
|
});
|
|
112
114
|
const app = createApp({ db, deploymentMode, allowedOrigins, getRuntimeHealth, realtimeHub });
|