bopodev-api 0.1.25 → 0.1.27
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/README.md +44 -0
- package/package.json +4 -4
- package/src/lib/run-artifact-paths.ts +80 -0
- package/src/pricing/anthropic.ts +23 -0
- package/src/pricing/gemini.ts +11 -0
- package/src/pricing/index.ts +29 -0
- package/src/pricing/openai.ts +47 -0
- package/src/pricing/opencode.ts +5 -0
- package/src/pricing/types.ts +8 -0
- package/src/routes/companies.ts +0 -2
- package/src/routes/governance.ts +80 -2
- package/src/routes/issues.ts +16 -1
- package/src/routes/observability.ts +79 -64
- package/src/scripts/onboard-seed.ts +6 -9
- package/src/server.ts +89 -3
- package/src/services/attention-service.ts +109 -48
- package/src/services/governance-service.ts +5 -5
- package/src/services/heartbeat-service.ts +1350 -86
- package/src/services/memory-file-service.ts +0 -3
- package/src/services/model-pricing.ts +4 -128
|
@@ -26,12 +26,10 @@ 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";
|
|
33
32
|
import { resolveDefaultRuntimeCwdForCompany } from "../lib/workspace-policy";
|
|
34
|
-
import { ensureCompanyModelPricingDefaults } from "../services/model-pricing";
|
|
35
33
|
import { applyTemplateManifest } from "../services/template-apply-service";
|
|
36
34
|
import { ensureCompanyBuiltinTemplateDefaults } from "../services/template-catalog";
|
|
37
35
|
|
|
@@ -212,8 +210,6 @@ export async function ensureOnboardingSeed(input: {
|
|
|
212
210
|
templateApplied = true;
|
|
213
211
|
appliedTemplateId = template.id;
|
|
214
212
|
}
|
|
215
|
-
await ensureCompanyModelPricingDefaults(db, companyId);
|
|
216
|
-
|
|
217
213
|
return {
|
|
218
214
|
companyId,
|
|
219
215
|
companyName: resolvedCompanyName,
|
|
@@ -305,15 +301,16 @@ async function ensureCeoStartupTask(
|
|
|
305
301
|
typeof issue.body === "string" &&
|
|
306
302
|
issue.body.includes(CEO_STARTUP_TASK_MARKER)
|
|
307
303
|
);
|
|
308
|
-
const
|
|
309
|
-
const ceoOperatingFolder = `${
|
|
310
|
-
const ceoTmpFolder = `${
|
|
304
|
+
const companyScopedCeoRoot = `workspace/${input.companyId}/agents/${input.ceoId}`;
|
|
305
|
+
const ceoOperatingFolder = `${companyScopedCeoRoot}/operating`;
|
|
306
|
+
const ceoTmpFolder = `${companyScopedCeoRoot}/tmp`;
|
|
311
307
|
const body = [
|
|
312
308
|
CEO_STARTUP_TASK_MARKER,
|
|
313
309
|
"",
|
|
314
310
|
"Stand up your leadership operating baseline before taking on additional delivery work.",
|
|
315
311
|
"",
|
|
316
|
-
`1. Create your operating folder at \`${ceoOperatingFolder}
|
|
312
|
+
`1. Create your operating folder at \`${ceoOperatingFolder}/\`.`,
|
|
313
|
+
" During heartbeats, prefer the absolute path in `$BOPODEV_AGENT_OPERATING_DIR` (set by the runtime) so files land under your agent folder even when the shell cwd is a project workspace.",
|
|
317
314
|
"2. Author these files with your own voice and responsibilities:",
|
|
318
315
|
` - \`${ceoOperatingFolder}/AGENTS.md\``,
|
|
319
316
|
` - \`${ceoOperatingFolder}/HEARTBEAT.md\``,
|
|
@@ -335,7 +332,7 @@ async function ensureCeoStartupTask(
|
|
|
335
332
|
"7. Do not use unsupported hire fields such as `adapterType`, `adapterConfig`, or `reportsTo`.",
|
|
336
333
|
"",
|
|
337
334
|
"Safety checks before requesting hire:",
|
|
338
|
-
|
|
335
|
+
`- Keep operating/system files inside \`workspace/${input.companyId}/agents/${input.ceoId}/\` only.`,
|
|
339
336
|
"- Do not request duplicates if a Founding Engineer already exists.",
|
|
340
337
|
"- Do not request duplicates if a pending approval for the same role is already open.",
|
|
341
338
|
"- 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
|
@@ -3,7 +3,7 @@ import { dirname, resolve } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { sql } from "drizzle-orm";
|
|
5
5
|
import { config as loadDotenv } from "dotenv";
|
|
6
|
-
import { bootstrapDatabase, listCompanies } from "bopodev-db";
|
|
6
|
+
import { bootstrapDatabase, listCompanies, resolveDefaultDbPath } from "bopodev-db";
|
|
7
7
|
import { checkRuntimeCommandHealth } from "bopodev-agent-sdk";
|
|
8
8
|
import type { RuntimeCommandHealth } from "bopodev-agent-sdk";
|
|
9
9
|
import { createApp } from "./app";
|
|
@@ -33,7 +33,28 @@ async function main() {
|
|
|
33
33
|
validateDeploymentConfiguration(deploymentMode, allowedOrigins, allowedHostnames, publicBaseUrl);
|
|
34
34
|
const dbPath = normalizeOptionalDbPath(process.env.BOPO_DB_PATH);
|
|
35
35
|
const port = Number(process.env.PORT ?? 4020);
|
|
36
|
-
const
|
|
36
|
+
const effectiveDbPath = dbPath ?? resolveDefaultDbPath();
|
|
37
|
+
let db: Awaited<ReturnType<typeof bootstrapDatabase>>["db"];
|
|
38
|
+
let dbClient: Awaited<ReturnType<typeof bootstrapDatabase>>["client"];
|
|
39
|
+
try {
|
|
40
|
+
const boot = await bootstrapDatabase(dbPath);
|
|
41
|
+
db = boot.db;
|
|
42
|
+
dbClient = boot.client;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (isProbablyPgliteWasmAbort(error)) {
|
|
45
|
+
// eslint-disable-next-line no-console
|
|
46
|
+
console.error(
|
|
47
|
+
"[startup] PGlite (embedded Postgres) failed during database bootstrap. This is unrelated to Codex or heartbeat prompt settings."
|
|
48
|
+
);
|
|
49
|
+
// eslint-disable-next-line no-console
|
|
50
|
+
console.error(`[startup] Data path in use: ${effectiveDbPath}`);
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.error(
|
|
53
|
+
"[startup] Recovery: stop all API/node processes using this DB, back up the path above, delete the file/dir, then restart (schema will be recreated). Or set BOPO_DB_PATH to a fresh path. See docs/operations/troubleshooting.md (PGlite)."
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
37
58
|
const existingCompanies = await listCompanies(db);
|
|
38
59
|
await ensureBuiltinPluginsRegistered(
|
|
39
60
|
db,
|
|
@@ -120,16 +141,69 @@ async function main() {
|
|
|
120
141
|
|
|
121
142
|
const defaultCompanyId = process.env.BOPO_DEFAULT_COMPANY_ID;
|
|
122
143
|
const schedulerCompanyId = await resolveSchedulerCompanyId(db, defaultCompanyId ?? null);
|
|
144
|
+
let stopScheduler: (() => void) | undefined;
|
|
123
145
|
if (schedulerCompanyId && shouldStartScheduler()) {
|
|
124
|
-
createHeartbeatScheduler(db, schedulerCompanyId, realtimeHub);
|
|
146
|
+
stopScheduler = createHeartbeatScheduler(db, schedulerCompanyId, realtimeHub);
|
|
125
147
|
} else if (schedulerCompanyId) {
|
|
126
148
|
// eslint-disable-next-line no-console
|
|
127
149
|
console.log("[startup] Scheduler disabled for this instance (BOPO_SCHEDULER_ROLE is follower/off).");
|
|
128
150
|
}
|
|
151
|
+
|
|
152
|
+
let shutdownInFlight: Promise<void> | null = null;
|
|
153
|
+
function shutdown(signal: string) {
|
|
154
|
+
shutdownInFlight ??= (async () => {
|
|
155
|
+
// eslint-disable-next-line no-console
|
|
156
|
+
console.log(`[shutdown] ${signal} — closing realtime, HTTP server, and embedded DB…`);
|
|
157
|
+
stopScheduler?.();
|
|
158
|
+
try {
|
|
159
|
+
await realtimeHub.close();
|
|
160
|
+
} catch (closeError) {
|
|
161
|
+
// eslint-disable-next-line no-console
|
|
162
|
+
console.error("[shutdown] realtime hub close error", closeError);
|
|
163
|
+
}
|
|
164
|
+
await new Promise<void>((resolve, reject) => {
|
|
165
|
+
server.close((err) => {
|
|
166
|
+
if (err) {
|
|
167
|
+
reject(err);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
resolve();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
try {
|
|
174
|
+
await closePgliteClient(dbClient);
|
|
175
|
+
} catch (closeDbError) {
|
|
176
|
+
// eslint-disable-next-line no-console
|
|
177
|
+
console.error("[shutdown] PGlite close error", closeDbError);
|
|
178
|
+
}
|
|
179
|
+
// eslint-disable-next-line no-console
|
|
180
|
+
console.log("[shutdown] clean exit");
|
|
181
|
+
process.exit(0);
|
|
182
|
+
})().catch((error) => {
|
|
183
|
+
// eslint-disable-next-line no-console
|
|
184
|
+
console.error("[shutdown] failed", error);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
});
|
|
187
|
+
return shutdownInFlight;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
191
|
+
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
129
192
|
}
|
|
130
193
|
|
|
131
194
|
void main();
|
|
132
195
|
|
|
196
|
+
async function closePgliteClient(client: unknown) {
|
|
197
|
+
if (!client || typeof client !== "object") {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const closeFn = (client as { close?: unknown }).close;
|
|
201
|
+
if (typeof closeFn !== "function") {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
await (closeFn as () => Promise<void>)();
|
|
205
|
+
}
|
|
206
|
+
|
|
133
207
|
async function hasCodexAgentsConfigured(db: Awaited<ReturnType<typeof bootstrapDatabase>>["db"]) {
|
|
134
208
|
const result = await db.execute(sql`
|
|
135
209
|
SELECT id
|
|
@@ -262,3 +336,15 @@ function normalizeOptionalDbPath(value: string | undefined) {
|
|
|
262
336
|
const normalized = value?.trim();
|
|
263
337
|
return normalized && normalized.length > 0 ? normalized : undefined;
|
|
264
338
|
}
|
|
339
|
+
|
|
340
|
+
function isProbablyPgliteWasmAbort(error: unknown): boolean {
|
|
341
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
342
|
+
const cause = error instanceof Error ? error.cause : undefined;
|
|
343
|
+
const causeMessage = cause instanceof Error ? cause.message : String(cause ?? "");
|
|
344
|
+
return (
|
|
345
|
+
message.includes("Aborted") ||
|
|
346
|
+
causeMessage.includes("Aborted") ||
|
|
347
|
+
message.includes("pglite") ||
|
|
348
|
+
causeMessage.includes("RuntimeError")
|
|
349
|
+
);
|
|
350
|
+
}
|
|
@@ -17,6 +17,40 @@ import type { BoardAttentionItem } from "bopodev-contracts";
|
|
|
17
17
|
|
|
18
18
|
type AttentionStateRow = Awaited<ReturnType<typeof listAttentionInboxStates>>[number];
|
|
19
19
|
|
|
20
|
+
/** Keep in sync with `RESOLVED_APPROVAL_INBOX_WINDOW_DAYS` in governance routes (resolved history in Inbox). */
|
|
21
|
+
const RESOLVED_APPROVAL_ATTENTION_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
type StoredApprovalRow = Awaited<ReturnType<typeof listApprovalRequests>>[number];
|
|
24
|
+
|
|
25
|
+
function approvalIncludedInAttentionList(approval: Pick<StoredApprovalRow, "status" | "resolvedAt">): boolean {
|
|
26
|
+
if (approval.status === "pending") {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (!approval.resolvedAt) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return Date.now() - approval.resolvedAt.getTime() <= RESOLVED_APPROVAL_ATTENTION_WINDOW_MS;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function finalizeApprovalAttentionItem(item: BoardAttentionItem, approval: Pick<StoredApprovalRow, "status" | "resolvedAt" | "action">): BoardAttentionItem {
|
|
36
|
+
if (approval.status === "pending") {
|
|
37
|
+
return item;
|
|
38
|
+
}
|
|
39
|
+
const outcome =
|
|
40
|
+
approval.status === "approved" ? "Approved" : approval.status === "rejected" ? "Rejected" : "Overridden";
|
|
41
|
+
const title =
|
|
42
|
+
approval.action === "override_budget" ? `Budget hard-stop · ${outcome}` : `Approval · ${outcome}`;
|
|
43
|
+
const resolvedAtIso = approval.resolvedAt?.toISOString() ?? item.resolvedAt;
|
|
44
|
+
return {
|
|
45
|
+
...item,
|
|
46
|
+
title,
|
|
47
|
+
state: "resolved",
|
|
48
|
+
resolvedAt: resolvedAtIso,
|
|
49
|
+
severity: "info",
|
|
50
|
+
sourceTimestamp: resolvedAtIso ?? item.sourceTimestamp
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
20
54
|
export async function listBoardAttentionItems(db: BopoDb, companyId: string, actorId: string): Promise<BoardAttentionItem[]> {
|
|
21
55
|
const [approvals, blockedIssues, heartbeatRuns, stateRows, boardComments] = await Promise.all([
|
|
22
56
|
listApprovalRequests(db, companyId),
|
|
@@ -42,7 +76,7 @@ export async function listBoardAttentionItems(db: BopoDb, companyId: string, act
|
|
|
42
76
|
const items: BoardAttentionItem[] = [];
|
|
43
77
|
|
|
44
78
|
for (const approval of approvals) {
|
|
45
|
-
if (approval
|
|
79
|
+
if (!approvalIncludedInAttentionList(approval)) {
|
|
46
80
|
continue;
|
|
47
81
|
}
|
|
48
82
|
const payload = parsePayload(approval.payloadJson);
|
|
@@ -55,25 +89,61 @@ export async function listBoardAttentionItems(db: BopoDb, companyId: string, act
|
|
|
55
89
|
const usedBudget = asNumber(payload.usedBudgetUsd);
|
|
56
90
|
const key = `budget:${approval.id}`;
|
|
57
91
|
items.push(
|
|
92
|
+
finalizeApprovalAttentionItem(
|
|
93
|
+
withState(
|
|
94
|
+
{
|
|
95
|
+
key,
|
|
96
|
+
category: "budget_hard_stop",
|
|
97
|
+
severity: ageHours >= 12 ? "critical" : "warning",
|
|
98
|
+
requiredActor: "board",
|
|
99
|
+
title: "Budget hard-stop requires board decision",
|
|
100
|
+
contextSummary: projectId
|
|
101
|
+
? `Project ${shortId(projectId)} is blocked by budget hard-stop.`
|
|
102
|
+
: agentId
|
|
103
|
+
? `Agent ${shortId(agentId)} is blocked by budget hard-stop.`
|
|
104
|
+
: "Agent work is blocked by budget hard-stop.",
|
|
105
|
+
actionLabel: "Review budget override",
|
|
106
|
+
actionHref: "/governance",
|
|
107
|
+
impactSummary: "Heartbeat work stays paused until budget override is approved or rejected.",
|
|
108
|
+
evidence: {
|
|
109
|
+
approvalId: approval.id,
|
|
110
|
+
projectId: projectId ?? undefined,
|
|
111
|
+
agentId: agentId ?? undefined
|
|
112
|
+
},
|
|
113
|
+
sourceTimestamp: approval.createdAt.toISOString(),
|
|
114
|
+
state: "open",
|
|
115
|
+
seenAt: null,
|
|
116
|
+
acknowledgedAt: null,
|
|
117
|
+
dismissedAt: null,
|
|
118
|
+
resolvedAt: null
|
|
119
|
+
},
|
|
120
|
+
stateByKey.get(key),
|
|
121
|
+
`Budget utilization ${formatPercent(utilizationPct)} (${formatUsd(usedBudget)} / ${formatUsd(currentBudget)}).`
|
|
122
|
+
),
|
|
123
|
+
approval
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const key = `approval:${approval.id}`;
|
|
130
|
+
items.push(
|
|
131
|
+
finalizeApprovalAttentionItem(
|
|
58
132
|
withState(
|
|
59
133
|
{
|
|
60
134
|
key,
|
|
61
|
-
category: "
|
|
62
|
-
severity: ageHours >=
|
|
135
|
+
category: "approval_required",
|
|
136
|
+
severity: ageHours >= 24 ? "critical" : "warning",
|
|
63
137
|
requiredActor: "board",
|
|
64
|
-
title: "
|
|
65
|
-
contextSummary:
|
|
66
|
-
|
|
67
|
-
: agentId
|
|
68
|
-
? `Agent ${shortId(agentId)} is blocked by budget hard-stop.`
|
|
69
|
-
: "Agent work is blocked by budget hard-stop.",
|
|
70
|
-
actionLabel: "Review budget override",
|
|
138
|
+
title: "Approval required",
|
|
139
|
+
contextSummary: formatApprovalContext(approval.action, payload),
|
|
140
|
+
actionLabel: "Open approvals",
|
|
71
141
|
actionHref: "/governance",
|
|
72
|
-
impactSummary: "
|
|
142
|
+
impactSummary: "Execution remains blocked until this governance decision is resolved.",
|
|
73
143
|
evidence: {
|
|
74
144
|
approvalId: approval.id,
|
|
75
|
-
projectId: projectId ?? undefined,
|
|
76
|
-
agentId: agentId ?? undefined
|
|
145
|
+
projectId: asString(payload.projectId) ?? undefined,
|
|
146
|
+
agentId: asString(payload.agentId) ?? undefined
|
|
77
147
|
},
|
|
78
148
|
sourceTimestamp: approval.createdAt.toISOString(),
|
|
79
149
|
state: "open",
|
|
@@ -82,39 +152,9 @@ export async function listBoardAttentionItems(db: BopoDb, companyId: string, act
|
|
|
82
152
|
dismissedAt: null,
|
|
83
153
|
resolvedAt: null
|
|
84
154
|
},
|
|
85
|
-
stateByKey.get(key)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const key = `approval:${approval.id}`;
|
|
93
|
-
items.push(
|
|
94
|
-
withState(
|
|
95
|
-
{
|
|
96
|
-
key,
|
|
97
|
-
category: "approval_required",
|
|
98
|
-
severity: ageHours >= 24 ? "critical" : "warning",
|
|
99
|
-
requiredActor: "board",
|
|
100
|
-
title: "Approval required",
|
|
101
|
-
contextSummary: formatApprovalContext(approval.action, payload),
|
|
102
|
-
actionLabel: "Open approvals",
|
|
103
|
-
actionHref: "/governance",
|
|
104
|
-
impactSummary: "Execution remains blocked until this governance decision is resolved.",
|
|
105
|
-
evidence: {
|
|
106
|
-
approvalId: approval.id,
|
|
107
|
-
projectId: asString(payload.projectId) ?? undefined,
|
|
108
|
-
agentId: asString(payload.agentId) ?? undefined
|
|
109
|
-
},
|
|
110
|
-
sourceTimestamp: approval.createdAt.toISOString(),
|
|
111
|
-
state: "open",
|
|
112
|
-
seenAt: null,
|
|
113
|
-
acknowledgedAt: null,
|
|
114
|
-
dismissedAt: null,
|
|
115
|
-
resolvedAt: null
|
|
116
|
-
},
|
|
117
|
-
stateByKey.get(key)
|
|
155
|
+
stateByKey.get(key)
|
|
156
|
+
),
|
|
157
|
+
approval
|
|
118
158
|
)
|
|
119
159
|
);
|
|
120
160
|
}
|
|
@@ -219,6 +259,7 @@ export async function listBoardAttentionItems(db: BopoDb, companyId: string, act
|
|
|
219
259
|
const key = `comment:${comment.id}`;
|
|
220
260
|
const body = comment.body.trim().replace(/\s+/g, " ");
|
|
221
261
|
const summaryBody = body.length > 140 ? `${body.slice(0, 137)}...` : body;
|
|
262
|
+
const conciseUsageLimitSummary = summarizeUsageLimitBoardComment(body);
|
|
222
263
|
items.push(
|
|
223
264
|
withState(
|
|
224
265
|
{
|
|
@@ -226,8 +267,8 @@ export async function listBoardAttentionItems(db: BopoDb, companyId: string, act
|
|
|
226
267
|
category: "board_mentioned_comment",
|
|
227
268
|
severity: "warning",
|
|
228
269
|
requiredActor: "board",
|
|
229
|
-
title: "Board input requested on issue comment",
|
|
230
|
-
contextSummary:
|
|
270
|
+
title: conciseUsageLimitSummary ? "Provider usage limit reached" : "Board input requested on issue comment",
|
|
271
|
+
contextSummary: conciseUsageLimitSummary ?? summaryBody,
|
|
231
272
|
actionLabel: "Open issue thread",
|
|
232
273
|
actionHref: `/issues/${comment.issueId}`,
|
|
233
274
|
impactSummary: "The team is waiting for board clarification to continue confidently.",
|
|
@@ -250,6 +291,26 @@ export async function listBoardAttentionItems(db: BopoDb, companyId: string, act
|
|
|
250
291
|
return dedupeItems(items).sort(compareAttentionItems);
|
|
251
292
|
}
|
|
252
293
|
|
|
294
|
+
function summarizeUsageLimitBoardComment(body: string) {
|
|
295
|
+
const normalized = body.replace(/\s+/g, " ").trim();
|
|
296
|
+
if (!normalized) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const providerMatch = normalized.match(
|
|
300
|
+
/\b(claude[_\s-]*code|codex|cursor|openai[_\s-]*api|anthropic[_\s-]*api|gemini[_\s-]*cli|opencode)\b(?=.*\busage limit reached\b)/i
|
|
301
|
+
);
|
|
302
|
+
if (!providerMatch) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const providerToken = providerMatch[1];
|
|
306
|
+
if (!providerToken) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const rawProvider = providerToken.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
310
|
+
const providerLabel = rawProvider.charAt(0).toUpperCase() + rawProvider.slice(1);
|
|
311
|
+
return `${providerLabel} usage limit reached.`;
|
|
312
|
+
}
|
|
313
|
+
|
|
253
314
|
export async function markBoardAttentionSeen(db: BopoDb, companyId: string, actorId: string, itemKey: string) {
|
|
254
315
|
await markAttentionInboxSeen(db, { companyId, actorId, itemKey });
|
|
255
316
|
}
|
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
import { resolveOpencodeRuntimeModel } from "../lib/opencode-model";
|
|
40
40
|
import {
|
|
41
41
|
normalizeCompanyWorkspacePath,
|
|
42
|
-
resolveAgentFallbackWorkspacePath,
|
|
43
42
|
resolveProjectWorkspacePath
|
|
44
43
|
} from "../lib/instance-paths";
|
|
45
44
|
import { assertRuntimeCwdForCompany, hasText, resolveDefaultRuntimeCwdForCompany } from "../lib/workspace-policy";
|
|
@@ -770,14 +769,15 @@ function resolveAgentDisplayTitle(title: string | null | undefined, roleKeyInput
|
|
|
770
769
|
}
|
|
771
770
|
|
|
772
771
|
function buildAgentStartupTaskBody(companyId: string, agentId: string) {
|
|
773
|
-
const
|
|
774
|
-
const agentOperatingFolder = `${
|
|
772
|
+
const companyScopedAgentRoot = `workspace/${companyId}/agents/${agentId}`;
|
|
773
|
+
const agentOperatingFolder = `${companyScopedAgentRoot}/operating`;
|
|
775
774
|
return [
|
|
776
775
|
AGENT_STARTUP_TASK_MARKER,
|
|
777
776
|
"",
|
|
778
777
|
`Create your operating baseline before starting feature delivery work.`,
|
|
779
778
|
"",
|
|
780
|
-
`1. Create your operating folder at \`${agentOperatingFolder}
|
|
779
|
+
`1. Create your operating folder at \`${agentOperatingFolder}/\`.`,
|
|
780
|
+
" During heartbeats, prefer the absolute path in `$BOPODEV_AGENT_OPERATING_DIR` (set by the runtime) so files land under your agent folder even when the shell cwd is a project workspace.",
|
|
781
781
|
"2. Author these files with your own responsibilities and working style:",
|
|
782
782
|
` - \`${agentOperatingFolder}/AGENTS.md\``,
|
|
783
783
|
` - \`${agentOperatingFolder}/HEARTBEAT.md\``,
|
|
@@ -787,7 +787,7 @@ function buildAgentStartupTaskBody(companyId: string, agentId: string) {
|
|
|
787
787
|
"4. Post an issue comment summarizing completed setup artifacts.",
|
|
788
788
|
"",
|
|
789
789
|
"Safety checks:",
|
|
790
|
-
|
|
790
|
+
`- Keep operating files inside \`workspace/${companyId}/agents/${agentId}/\` only.`,
|
|
791
791
|
"- Do not overwrite another agent's operating folder.",
|
|
792
792
|
"- Keep content original to your role and scope."
|
|
793
793
|
].join("\n");
|