bopodev-api 0.1.25 → 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/routes/governance.ts +80 -2
- package/src/routes/observability.ts +157 -9
- package/src/scripts/onboard-seed.ts +5 -6
- package/src/services/attention-service.ts +23 -2
- package/src/services/governance-service.ts +4 -5
- package/src/services/heartbeat-service.ts +1251 -69
- package/src/services/memory-file-service.ts +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bopodev-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
"nanoid": "^5.1.5",
|
|
18
18
|
"ws": "^8.19.0",
|
|
19
19
|
"zod": "^4.1.5",
|
|
20
|
-
"bopodev-
|
|
21
|
-
"bopodev-
|
|
22
|
-
"bopodev-
|
|
20
|
+
"bopodev-db": "0.1.26",
|
|
21
|
+
"bopodev-agent-sdk": "0.1.26",
|
|
22
|
+
"bopodev-contracts": "0.1.26"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/cors": "^2.8.19",
|
package/src/routes/governance.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
|
+
addIssueComment,
|
|
4
5
|
appendAuditEvent,
|
|
5
6
|
clearApprovalInboxDismissed,
|
|
6
7
|
countPendingApprovalRequests,
|
|
@@ -209,6 +210,36 @@ export function createGovernanceRouter(ctx: AppContext) {
|
|
|
209
210
|
if (approval.requestedByAgentId) {
|
|
210
211
|
await publishOfficeOccupantForAgent(ctx.db, ctx.realtimeHub, req.companyId!, approval.requestedByAgentId);
|
|
211
212
|
}
|
|
213
|
+
if (parsed.data.status === "approved" && resolution.action === "hire_agent" && resolution.execution.applied) {
|
|
214
|
+
const hireContext = parseHireApprovalCommentContext(approval.payloadJson);
|
|
215
|
+
if (hireContext.issueIds.length > 0) {
|
|
216
|
+
const commentBody = buildHireApprovalIssueComment(hireContext.roleLabel);
|
|
217
|
+
try {
|
|
218
|
+
for (const issueId of hireContext.issueIds) {
|
|
219
|
+
await addIssueComment(ctx.db, {
|
|
220
|
+
companyId: req.companyId!,
|
|
221
|
+
issueId,
|
|
222
|
+
body: commentBody,
|
|
223
|
+
authorType: auditActor.actorType === "agent" ? "agent" : "human",
|
|
224
|
+
authorId: auditActor.actorId
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
await appendAuditEvent(ctx.db, {
|
|
229
|
+
companyId: req.companyId!,
|
|
230
|
+
actorType: "system",
|
|
231
|
+
actorId: null,
|
|
232
|
+
eventType: "governance.hire_approval_comment_failed",
|
|
233
|
+
entityType: "approval_request",
|
|
234
|
+
entityId: approval.id,
|
|
235
|
+
payload: {
|
|
236
|
+
error: String(error),
|
|
237
|
+
issueIds: hireContext.issueIds
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
212
243
|
}
|
|
213
244
|
|
|
214
245
|
if (resolution.execution.entityType === "agent" && resolution.execution.entityId) {
|
|
@@ -231,11 +262,58 @@ function resolveAuditActor(actor: { type: "board" | "member" | "agent"; id: stri
|
|
|
231
262
|
return { actorType: "human" as const, actorId: actor.id };
|
|
232
263
|
}
|
|
233
264
|
|
|
234
|
-
function parsePayload(payloadJson: string) {
|
|
265
|
+
function parsePayload(payloadJson: string): Record<string, unknown> {
|
|
235
266
|
try {
|
|
236
267
|
const parsed = JSON.parse(payloadJson) as unknown;
|
|
237
|
-
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
268
|
+
return typeof parsed === "object" && parsed !== null ? (parsed as Record<string, unknown>) : {};
|
|
238
269
|
} catch {
|
|
239
270
|
return {};
|
|
240
271
|
}
|
|
241
272
|
}
|
|
273
|
+
|
|
274
|
+
function parseHireApprovalCommentContext(payloadJson: string) {
|
|
275
|
+
const payload = parsePayload(payloadJson);
|
|
276
|
+
const issueIds = normalizeSourceIssueIds(
|
|
277
|
+
typeof payload.sourceIssueId === "string" ? payload.sourceIssueId : undefined,
|
|
278
|
+
Array.isArray(payload.sourceIssueIds) ? payload.sourceIssueIds : undefined
|
|
279
|
+
);
|
|
280
|
+
const roleLabel = resolveHireRoleLabel(payload);
|
|
281
|
+
return { issueIds, roleLabel };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function normalizeSourceIssueIds(sourceIssueId?: string, sourceIssueIds?: unknown[]) {
|
|
285
|
+
const normalized = new Set<string>();
|
|
286
|
+
for (const entry of [sourceIssueId, ...(sourceIssueIds ?? [])]) {
|
|
287
|
+
if (typeof entry !== "string") {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const trimmed = entry.trim();
|
|
291
|
+
if (trimmed.length > 0) {
|
|
292
|
+
normalized.add(trimmed);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return Array.from(normalized);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function resolveHireRoleLabel(payload: Record<string, unknown>) {
|
|
299
|
+
const title = typeof payload.title === "string" ? payload.title.trim() : "";
|
|
300
|
+
if (title.length > 0) {
|
|
301
|
+
return title;
|
|
302
|
+
}
|
|
303
|
+
const role = typeof payload.role === "string" ? payload.role.trim() : "";
|
|
304
|
+
if (role.length > 0) {
|
|
305
|
+
return role;
|
|
306
|
+
}
|
|
307
|
+
const roleKey = typeof payload.roleKey === "string" ? payload.roleKey.trim() : "";
|
|
308
|
+
if (roleKey.length > 0) {
|
|
309
|
+
return roleKey.replace(/_/g, " ");
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function buildHireApprovalIssueComment(roleLabel: string | null) {
|
|
315
|
+
if (roleLabel) {
|
|
316
|
+
return `Approved hiring of ${roleLabel}.`;
|
|
317
|
+
}
|
|
318
|
+
return "Approved hiring request.";
|
|
319
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
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,
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
} from "bopodev-db";
|
|
16
18
|
import type { AppContext } from "../context";
|
|
17
19
|
import { sendError, sendOk } from "../http";
|
|
20
|
+
import { isInsidePath, resolveCompanyWorkspaceRootPath } from "../lib/instance-paths";
|
|
18
21
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
19
22
|
import { requirePermission } from "../middleware/request-actor";
|
|
20
23
|
import { listAgentMemoryFiles, loadAgentMemoryContext, readAgentMemoryFile } from "../services/memory-file-service";
|
|
@@ -113,10 +116,12 @@ export function createObservabilityRouter(ctx: AppContext) {
|
|
|
113
116
|
.filter((run) => (agentFilter ? run.agentId === agentFilter : true))
|
|
114
117
|
.map((run) => {
|
|
115
118
|
const details = runDetailsByRunId.get(run.id);
|
|
116
|
-
const
|
|
119
|
+
const report = toRecord(details?.report);
|
|
120
|
+
const outcome = details?.outcome ?? report?.outcome ?? null;
|
|
117
121
|
return {
|
|
118
|
-
...serializeRunRow(run,
|
|
119
|
-
outcome
|
|
122
|
+
...serializeRunRow(run, details),
|
|
123
|
+
outcome,
|
|
124
|
+
report: report ?? null
|
|
120
125
|
};
|
|
121
126
|
})
|
|
122
127
|
);
|
|
@@ -138,7 +143,7 @@ export function createObservabilityRouter(ctx: AppContext) {
|
|
|
138
143
|
const trace = toRecord(details?.trace);
|
|
139
144
|
const traceTranscript = Array.isArray(trace?.transcript) ? trace.transcript : [];
|
|
140
145
|
return sendOk(res, {
|
|
141
|
-
run: serializeRunRow(run, details
|
|
146
|
+
run: serializeRunRow(run, details),
|
|
142
147
|
details,
|
|
143
148
|
transcript: {
|
|
144
149
|
hasPersistedMessages: transcriptResult.items.length > 0,
|
|
@@ -148,6 +153,46 @@ export function createObservabilityRouter(ctx: AppContext) {
|
|
|
148
153
|
});
|
|
149
154
|
});
|
|
150
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
|
+
|
|
151
196
|
router.get("/heartbeats/:runId/messages", async (req, res) => {
|
|
152
197
|
const companyId = req.companyId!;
|
|
153
198
|
const runId = req.params.runId;
|
|
@@ -359,6 +404,85 @@ function toRecord(value: unknown) {
|
|
|
359
404
|
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
|
|
360
405
|
}
|
|
361
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
|
+
|
|
362
486
|
function serializeRunRow(
|
|
363
487
|
run: {
|
|
364
488
|
id: string;
|
|
@@ -369,14 +493,27 @@ function serializeRunRow(
|
|
|
369
493
|
finishedAt: Date | null;
|
|
370
494
|
message: string | null;
|
|
371
495
|
},
|
|
372
|
-
|
|
496
|
+
details: Record<string, unknown> | null | undefined
|
|
373
497
|
) {
|
|
374
|
-
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";
|
|
375
511
|
return {
|
|
376
512
|
id: run.id,
|
|
377
513
|
companyId: run.companyId,
|
|
378
514
|
agentId: run.agentId,
|
|
379
515
|
status: run.status,
|
|
516
|
+
publicStatus,
|
|
380
517
|
startedAt: run.startedAt.toISOString(),
|
|
381
518
|
finishedAt: run.finishedAt?.toISOString() ?? null,
|
|
382
519
|
message: run.message ?? null,
|
|
@@ -389,14 +526,25 @@ function resolveRunType(
|
|
|
389
526
|
status: string;
|
|
390
527
|
message: string | null;
|
|
391
528
|
},
|
|
392
|
-
|
|
529
|
+
details: Record<string, unknown> | null | undefined
|
|
393
530
|
): "work" | "no_assigned_work" | "budget_skip" | "overlap_skip" | "other_skip" | "failed" | "running" {
|
|
394
531
|
if (run.status === "started") {
|
|
395
532
|
return "running";
|
|
396
533
|
}
|
|
397
|
-
|
|
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") {
|
|
398
537
|
return "failed";
|
|
399
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
|
+
}
|
|
400
548
|
const normalizedMessage = (run.message ?? "").toLowerCase();
|
|
401
549
|
if (normalizedMessage.includes("already in progress")) {
|
|
402
550
|
return "overlap_skip";
|
|
@@ -407,7 +555,7 @@ function resolveRunType(
|
|
|
407
555
|
if (isNoAssignedWorkMessage(run.message)) {
|
|
408
556
|
return "no_assigned_work";
|
|
409
557
|
}
|
|
410
|
-
if (isNoAssignedWorkOutcome(outcome)) {
|
|
558
|
+
if (isNoAssignedWorkOutcome(details?.outcome)) {
|
|
411
559
|
return "no_assigned_work";
|
|
412
560
|
}
|
|
413
561
|
if (run.status === "skipped") {
|
|
@@ -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";
|
|
@@ -305,15 +304,15 @@ async function ensureCeoStartupTask(
|
|
|
305
304
|
typeof issue.body === "string" &&
|
|
306
305
|
issue.body.includes(CEO_STARTUP_TASK_MARKER)
|
|
307
306
|
);
|
|
308
|
-
const
|
|
309
|
-
const ceoOperatingFolder = `${
|
|
310
|
-
const ceoTmpFolder = `${
|
|
307
|
+
const companyScopedCeoRoot = `workspace/${input.companyId}/agents/${input.ceoId}`;
|
|
308
|
+
const ceoOperatingFolder = `${companyScopedCeoRoot}/operating`;
|
|
309
|
+
const ceoTmpFolder = `${companyScopedCeoRoot}/tmp`;
|
|
311
310
|
const body = [
|
|
312
311
|
CEO_STARTUP_TASK_MARKER,
|
|
313
312
|
"",
|
|
314
313
|
"Stand up your leadership operating baseline before taking on additional delivery work.",
|
|
315
314
|
"",
|
|
316
|
-
`1. Create your operating folder at \`${ceoOperatingFolder}
|
|
315
|
+
`1. Create your operating folder at \`${ceoOperatingFolder}/\`.`,
|
|
317
316
|
"2. Author these files with your own voice and responsibilities:",
|
|
318
317
|
` - \`${ceoOperatingFolder}/AGENTS.md\``,
|
|
319
318
|
` - \`${ceoOperatingFolder}/HEARTBEAT.md\``,
|
|
@@ -335,7 +334,7 @@ async function ensureCeoStartupTask(
|
|
|
335
334
|
"7. Do not use unsupported hire fields such as `adapterType`, `adapterConfig`, or `reportsTo`.",
|
|
336
335
|
"",
|
|
337
336
|
"Safety checks before requesting hire:",
|
|
338
|
-
|
|
337
|
+
`- Keep operating/system files inside \`workspace/${input.companyId}/agents/${input.ceoId}/\` only.`,
|
|
339
338
|
"- Do not request duplicates if a Founding Engineer already exists.",
|
|
340
339
|
"- Do not request duplicates if a pending approval for the same role is already open.",
|
|
341
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`.",
|
|
@@ -219,6 +219,7 @@ export async function listBoardAttentionItems(db: BopoDb, companyId: string, act
|
|
|
219
219
|
const key = `comment:${comment.id}`;
|
|
220
220
|
const body = comment.body.trim().replace(/\s+/g, " ");
|
|
221
221
|
const summaryBody = body.length > 140 ? `${body.slice(0, 137)}...` : body;
|
|
222
|
+
const conciseUsageLimitSummary = summarizeUsageLimitBoardComment(body);
|
|
222
223
|
items.push(
|
|
223
224
|
withState(
|
|
224
225
|
{
|
|
@@ -226,8 +227,8 @@ export async function listBoardAttentionItems(db: BopoDb, companyId: string, act
|
|
|
226
227
|
category: "board_mentioned_comment",
|
|
227
228
|
severity: "warning",
|
|
228
229
|
requiredActor: "board",
|
|
229
|
-
title: "Board input requested on issue comment",
|
|
230
|
-
contextSummary:
|
|
230
|
+
title: conciseUsageLimitSummary ? "Provider usage limit reached" : "Board input requested on issue comment",
|
|
231
|
+
contextSummary: conciseUsageLimitSummary ?? summaryBody,
|
|
231
232
|
actionLabel: "Open issue thread",
|
|
232
233
|
actionHref: `/issues/${comment.issueId}`,
|
|
233
234
|
impactSummary: "The team is waiting for board clarification to continue confidently.",
|
|
@@ -250,6 +251,26 @@ export async function listBoardAttentionItems(db: BopoDb, companyId: string, act
|
|
|
250
251
|
return dedupeItems(items).sort(compareAttentionItems);
|
|
251
252
|
}
|
|
252
253
|
|
|
254
|
+
function summarizeUsageLimitBoardComment(body: string) {
|
|
255
|
+
const normalized = body.replace(/\s+/g, " ").trim();
|
|
256
|
+
if (!normalized) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const providerMatch = normalized.match(
|
|
260
|
+
/\b(claude[_\s-]*code|codex|cursor|openai[_\s-]*api|anthropic[_\s-]*api|gemini[_\s-]*cli|opencode)\b(?=.*\busage limit reached\b)/i
|
|
261
|
+
);
|
|
262
|
+
if (!providerMatch) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const providerToken = providerMatch[1];
|
|
266
|
+
if (!providerToken) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const rawProvider = providerToken.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
270
|
+
const providerLabel = rawProvider.charAt(0).toUpperCase() + rawProvider.slice(1);
|
|
271
|
+
return `${providerLabel} usage limit reached.`;
|
|
272
|
+
}
|
|
273
|
+
|
|
253
274
|
export async function markBoardAttentionSeen(db: BopoDb, companyId: string, actorId: string, itemKey: string) {
|
|
254
275
|
await markAttentionInboxSeen(db, { companyId, actorId, itemKey });
|
|
255
276
|
}
|
|
@@ -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,14 @@ 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}/\`.`,
|
|
781
780
|
"2. Author these files with your own responsibilities and working style:",
|
|
782
781
|
` - \`${agentOperatingFolder}/AGENTS.md\``,
|
|
783
782
|
` - \`${agentOperatingFolder}/HEARTBEAT.md\``,
|
|
@@ -787,7 +786,7 @@ function buildAgentStartupTaskBody(companyId: string, agentId: string) {
|
|
|
787
786
|
"4. Post an issue comment summarizing completed setup artifacts.",
|
|
788
787
|
"",
|
|
789
788
|
"Safety checks:",
|
|
790
|
-
|
|
789
|
+
`- Keep operating files inside \`workspace/${companyId}/agents/${agentId}/\` only.`,
|
|
791
790
|
"- Do not overwrite another agent's operating folder.",
|
|
792
791
|
"- Keep content original to your role and scope."
|
|
793
792
|
].join("\n");
|