bopodev-api 0.1.28 → 0.1.30
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 +17 -69
- package/src/lib/ceo-bootstrap-prompt.ts +1 -0
- package/src/lib/run-artifact-paths.ts +8 -0
- package/src/middleware/cors-config.ts +36 -0
- package/src/middleware/request-actor.ts +10 -16
- package/src/middleware/request-id.ts +9 -0
- package/src/middleware/request-logging.ts +24 -0
- package/src/realtime/office-space.ts +1 -0
- package/src/routes/agents.ts +90 -46
- package/src/routes/companies.ts +20 -1
- package/src/routes/goals.ts +7 -13
- package/src/routes/governance.ts +2 -5
- package/src/routes/heartbeats.ts +7 -25
- package/src/routes/issues.ts +65 -120
- package/src/routes/observability.ts +6 -1
- package/src/routes/plugins.ts +5 -17
- package/src/routes/projects.ts +7 -25
- package/src/routes/templates.ts +6 -21
- package/src/scripts/onboard-seed.ts +18 -8
- package/src/server.ts +33 -292
- package/src/services/company-export-service.ts +63 -0
- package/src/services/governance-service.ts +10 -14
- package/src/services/heartbeat-service/active-runs.ts +15 -0
- package/src/services/heartbeat-service/budget-override.ts +46 -0
- package/src/services/heartbeat-service/claims.ts +61 -0
- package/src/services/heartbeat-service/cron.ts +58 -0
- package/src/services/heartbeat-service/heartbeat-realtime.ts +28 -0
- package/src/services/heartbeat-service/heartbeat-run-summary-text.ts +53 -0
- package/src/services/{heartbeat-service.ts → heartbeat-service/heartbeat-run.ts} +201 -634
- package/src/services/heartbeat-service/index.ts +5 -0
- package/src/services/heartbeat-service/stop.ts +90 -0
- package/src/services/heartbeat-service/sweep.ts +145 -0
- package/src/services/heartbeat-service/types.ts +66 -0
- package/src/services/memory-file-service.ts +10 -2
- package/src/services/template-apply-service.ts +6 -0
- package/src/services/template-catalog.ts +37 -3
- package/src/shutdown/graceful-shutdown.ts +77 -0
- package/src/startup/database.ts +41 -0
- package/src/startup/deployment-validation.ts +37 -0
- package/src/startup/env.ts +17 -0
- package/src/startup/runtime-health.ts +128 -0
- package/src/startup/scheduler-config.ts +39 -0
- package/src/types/express.d.ts +13 -0
- package/src/types/request-actor.ts +6 -0
- package/src/validation/issue-routes.ts +80 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { agents, eq } from "bopodev-db";
|
|
2
|
+
import { checkRuntimeCommandHealth } from "bopodev-agent-sdk";
|
|
3
|
+
import type { RuntimeCommandHealth } from "bopodev-agent-sdk";
|
|
4
|
+
import type { BootstrappedDb } from "./database";
|
|
5
|
+
|
|
6
|
+
type BopoDb = BootstrappedDb["db"];
|
|
7
|
+
|
|
8
|
+
export async function hasCodexAgentsConfigured(db: BopoDb) {
|
|
9
|
+
const result = await db
|
|
10
|
+
.select({ id: agents.id })
|
|
11
|
+
.from(agents)
|
|
12
|
+
.where(eq(agents.providerType, "codex"))
|
|
13
|
+
.limit(1);
|
|
14
|
+
return result.length > 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function hasOpenCodeAgentsConfigured(db: BopoDb) {
|
|
18
|
+
const result = await db
|
|
19
|
+
.select({ id: agents.id })
|
|
20
|
+
.from(agents)
|
|
21
|
+
.where(eq(agents.providerType, "opencode"))
|
|
22
|
+
.limit(1);
|
|
23
|
+
return result.length > 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function emitCodexPreflightWarning(health: RuntimeCommandHealth) {
|
|
27
|
+
const red = process.stderr.isTTY ? "\x1b[31m" : "";
|
|
28
|
+
const yellow = process.stderr.isTTY ? "\x1b[33m" : "";
|
|
29
|
+
const reset = process.stderr.isTTY ? "\x1b[0m" : "";
|
|
30
|
+
const symbol = `${red}✖${reset}`;
|
|
31
|
+
process.stderr.write(
|
|
32
|
+
`${symbol} ${yellow}Codex preflight failed${reset}: command '${health.command}' is unavailable.\n`
|
|
33
|
+
);
|
|
34
|
+
process.stderr.write(` Install Codex CLI or set BOPO_SKIP_CODEX_PREFLIGHT=1 for local dev.\n`);
|
|
35
|
+
if (process.env.BOPO_VERBOSE_STARTUP_WARNINGS === "1") {
|
|
36
|
+
process.stderr.write(` Details: ${JSON.stringify(health)}\n`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function emitOpenCodePreflightWarning(health: RuntimeCommandHealth) {
|
|
41
|
+
const red = process.stderr.isTTY ? "\x1b[31m" : "";
|
|
42
|
+
const yellow = process.stderr.isTTY ? "\x1b[33m" : "";
|
|
43
|
+
const reset = process.stderr.isTTY ? "\x1b[0m" : "";
|
|
44
|
+
const symbol = `${red}✖${reset}`;
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
`${symbol} ${yellow}OpenCode preflight failed${reset}: command '${health.command}' is unavailable.\n`
|
|
47
|
+
);
|
|
48
|
+
process.stderr.write(` Install OpenCode CLI or set BOPO_SKIP_OPENCODE_PREFLIGHT=1 for local dev.\n`);
|
|
49
|
+
if (process.env.BOPO_VERBOSE_STARTUP_WARNINGS === "1") {
|
|
50
|
+
process.stderr.write(` Details: ${JSON.stringify(health)}\n`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runStartupRuntimePreflights(options: {
|
|
55
|
+
codexHealthRequired: boolean;
|
|
56
|
+
openCodeHealthRequired: boolean;
|
|
57
|
+
codexCommand: string;
|
|
58
|
+
openCodeCommand: string;
|
|
59
|
+
}) {
|
|
60
|
+
const { codexHealthRequired, openCodeHealthRequired, codexCommand, openCodeCommand } = options;
|
|
61
|
+
if (codexHealthRequired) {
|
|
62
|
+
const startupCodexHealth = await checkRuntimeCommandHealth(codexCommand, {
|
|
63
|
+
timeoutMs: 5_000
|
|
64
|
+
});
|
|
65
|
+
if (!startupCodexHealth.available) {
|
|
66
|
+
emitCodexPreflightWarning(startupCodexHealth);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (openCodeHealthRequired) {
|
|
70
|
+
const startupOpenCodeHealth = await checkRuntimeCommandHealth(openCodeCommand, {
|
|
71
|
+
timeoutMs: 5_000
|
|
72
|
+
});
|
|
73
|
+
if (!startupOpenCodeHealth.available) {
|
|
74
|
+
emitOpenCodePreflightWarning(startupOpenCodeHealth);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function buildGetRuntimeHealth(options: {
|
|
80
|
+
codexCommand: string;
|
|
81
|
+
openCodeCommand: string;
|
|
82
|
+
skipCodexPreflight: boolean;
|
|
83
|
+
skipOpenCodePreflight: boolean;
|
|
84
|
+
codexHealthRequired: boolean;
|
|
85
|
+
openCodeHealthRequired: boolean;
|
|
86
|
+
}) {
|
|
87
|
+
const {
|
|
88
|
+
codexCommand,
|
|
89
|
+
openCodeCommand,
|
|
90
|
+
skipCodexPreflight,
|
|
91
|
+
skipOpenCodePreflight,
|
|
92
|
+
codexHealthRequired,
|
|
93
|
+
openCodeHealthRequired
|
|
94
|
+
} = options;
|
|
95
|
+
|
|
96
|
+
return async () => {
|
|
97
|
+
const codex = codexHealthRequired
|
|
98
|
+
? await checkRuntimeCommandHealth(codexCommand, {
|
|
99
|
+
timeoutMs: 5_000
|
|
100
|
+
})
|
|
101
|
+
: {
|
|
102
|
+
command: codexCommand,
|
|
103
|
+
available: skipCodexPreflight ? false : true,
|
|
104
|
+
exitCode: null,
|
|
105
|
+
elapsedMs: 0,
|
|
106
|
+
error: skipCodexPreflight
|
|
107
|
+
? "Skipped by configuration: BOPO_SKIP_CODEX_PREFLIGHT=1."
|
|
108
|
+
: "Skipped: no Codex agents configured."
|
|
109
|
+
};
|
|
110
|
+
const opencode = openCodeHealthRequired
|
|
111
|
+
? await checkRuntimeCommandHealth(openCodeCommand, {
|
|
112
|
+
timeoutMs: 5_000
|
|
113
|
+
})
|
|
114
|
+
: {
|
|
115
|
+
command: openCodeCommand,
|
|
116
|
+
available: skipOpenCodePreflight ? false : true,
|
|
117
|
+
exitCode: null,
|
|
118
|
+
elapsedMs: 0,
|
|
119
|
+
error: skipOpenCodePreflight
|
|
120
|
+
? "Skipped by configuration: BOPO_SKIP_OPENCODE_PREFLIGHT=1."
|
|
121
|
+
: "Skipped: no OpenCode agents configured."
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
codex,
|
|
125
|
+
opencode
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { asc, companies, eq } from "bopodev-db";
|
|
2
|
+
import type { BootstrappedDb } from "./database";
|
|
3
|
+
|
|
4
|
+
export async function resolveSchedulerCompanyId(
|
|
5
|
+
db: BootstrappedDb["db"],
|
|
6
|
+
configuredCompanyId: string | null
|
|
7
|
+
) {
|
|
8
|
+
if (configuredCompanyId) {
|
|
9
|
+
const configured = await db
|
|
10
|
+
.select({ id: companies.id })
|
|
11
|
+
.from(companies)
|
|
12
|
+
.where(eq(companies.id, configuredCompanyId))
|
|
13
|
+
.limit(1);
|
|
14
|
+
if (configured.length > 0) {
|
|
15
|
+
return configuredCompanyId;
|
|
16
|
+
}
|
|
17
|
+
// eslint-disable-next-line no-console
|
|
18
|
+
console.warn(`[startup] BOPO_DEFAULT_COMPANY_ID='${configuredCompanyId}' was not found; using first available company.`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const fallback = await db
|
|
22
|
+
.select({ id: companies.id })
|
|
23
|
+
.from(companies)
|
|
24
|
+
.orderBy(asc(companies.createdAt))
|
|
25
|
+
.limit(1);
|
|
26
|
+
const id = fallback[0]?.id;
|
|
27
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function shouldStartScheduler() {
|
|
31
|
+
const rawRole = (process.env.BOPO_SCHEDULER_ROLE ?? "auto").trim().toLowerCase();
|
|
32
|
+
if (rawRole === "off" || rawRole === "follower") {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (rawRole === "leader" || rawRole === "auto") {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Invalid BOPO_SCHEDULER_ROLE '${rawRole}'. Expected one of: auto, leader, follower, off.`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const createIssueSchema = z.object({
|
|
4
|
+
projectId: z.string().min(1),
|
|
5
|
+
parentIssueId: z.string().optional(),
|
|
6
|
+
title: z.string().min(1),
|
|
7
|
+
body: z.string().optional(),
|
|
8
|
+
metadata: z
|
|
9
|
+
.object({
|
|
10
|
+
delegatedHiringIntent: z
|
|
11
|
+
.object({
|
|
12
|
+
intentType: z.literal("agent_hiring_request"),
|
|
13
|
+
requestedRole: z.string().nullable().optional(),
|
|
14
|
+
requestedRoleKey: z.string().nullable().optional(),
|
|
15
|
+
requestedTitle: z.string().nullable().optional(),
|
|
16
|
+
requestedName: z.string().nullable().optional(),
|
|
17
|
+
requestedManagerAgentId: z.string().nullable().optional(),
|
|
18
|
+
requestedProviderType: z.string().nullable().optional(),
|
|
19
|
+
requestedRuntimeModel: z.string().nullable().optional(),
|
|
20
|
+
requestedCapabilities: z.string().max(4000).nullable().optional()
|
|
21
|
+
})
|
|
22
|
+
.optional()
|
|
23
|
+
})
|
|
24
|
+
.optional(),
|
|
25
|
+
status: z.enum(["todo", "in_progress", "blocked", "in_review", "done", "canceled"]).default("todo"),
|
|
26
|
+
priority: z.enum(["none", "low", "medium", "high", "urgent"]).default("none"),
|
|
27
|
+
assigneeAgentId: z.string().nullable().optional(),
|
|
28
|
+
goalIds: z.array(z.string().min(1)).default([]),
|
|
29
|
+
externalLink: z.string().max(2048).nullable().optional(),
|
|
30
|
+
labels: z.array(z.string()).default([]),
|
|
31
|
+
tags: z.array(z.string()).default([])
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const createIssueCommentSchema = z.object({
|
|
35
|
+
body: z.string().min(1),
|
|
36
|
+
recipients: z
|
|
37
|
+
.array(
|
|
38
|
+
z.object({
|
|
39
|
+
recipientType: z.enum(["agent", "board", "member"]),
|
|
40
|
+
recipientId: z.string().nullable().optional()
|
|
41
|
+
})
|
|
42
|
+
)
|
|
43
|
+
.default([]),
|
|
44
|
+
authorType: z.enum(["human", "agent", "system"]).optional(),
|
|
45
|
+
authorId: z.string().optional()
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const createIssueCommentLegacySchema = z.object({
|
|
49
|
+
issueId: z.string().min(1),
|
|
50
|
+
body: z.string().min(1),
|
|
51
|
+
recipients: z
|
|
52
|
+
.array(
|
|
53
|
+
z.object({
|
|
54
|
+
recipientType: z.enum(["agent", "board", "member"]),
|
|
55
|
+
recipientId: z.string().nullable().optional()
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
.default([]),
|
|
59
|
+
authorType: z.enum(["human", "agent", "system"]).optional(),
|
|
60
|
+
authorId: z.string().optional()
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const updateIssueCommentSchema = z.object({
|
|
64
|
+
body: z.string().min(1)
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const updateIssueSchema = z
|
|
68
|
+
.object({
|
|
69
|
+
projectId: z.string().min(1).optional(),
|
|
70
|
+
title: z.string().min(1).optional(),
|
|
71
|
+
body: z.string().nullable().optional(),
|
|
72
|
+
status: z.enum(["todo", "in_progress", "blocked", "in_review", "done", "canceled"]).optional(),
|
|
73
|
+
priority: z.enum(["none", "low", "medium", "high", "urgent"]).optional(),
|
|
74
|
+
assigneeAgentId: z.string().nullable().optional(),
|
|
75
|
+
goalIds: z.array(z.string().min(1)).optional(),
|
|
76
|
+
externalLink: z.string().max(2048).nullable().optional(),
|
|
77
|
+
labels: z.array(z.string()).optional(),
|
|
78
|
+
tags: z.array(z.string()).optional()
|
|
79
|
+
})
|
|
80
|
+
.refine((payload) => Object.keys(payload).length > 0, "At least one field must be provided.");
|