bopodev-api 0.1.32 → 0.1.33
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 +6 -4
- package/src/lib/agent-config.ts +58 -10
- package/src/lib/builtin-bopo-skills/bopodev-control-plane.md +123 -0
- package/src/lib/builtin-bopo-skills/bopodev-create-agent.md +90 -0
- package/src/lib/builtin-bopo-skills/index.ts +36 -0
- package/src/lib/builtin-bopo-skills/para-memory-files.md +48 -0
- package/src/lib/instance-paths.ts +5 -0
- package/src/routes/agents.ts +21 -7
- package/src/routes/observability.ts +167 -0
- package/src/services/company-file-archive-service.ts +2 -1
- package/src/services/company-skill-file-service.ts +558 -0
- package/src/services/governance-service.ts +11 -3
- package/src/services/heartbeat-service/heartbeat-run.ts +45 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bopodev-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -17,18 +17,20 @@
|
|
|
17
17
|
"fflate": "^0.8.2",
|
|
18
18
|
"multer": "^2.1.1",
|
|
19
19
|
"nanoid": "^5.1.5",
|
|
20
|
+
"turndown": "^7.2.2",
|
|
20
21
|
"ws": "^8.19.0",
|
|
21
22
|
"yaml": "^2.8.3",
|
|
22
23
|
"zod": "^4.1.5",
|
|
23
|
-
"bopodev-agent-sdk": "0.1.
|
|
24
|
-
"bopodev-
|
|
25
|
-
"bopodev-
|
|
24
|
+
"bopodev-agent-sdk": "0.1.33",
|
|
25
|
+
"bopodev-db": "0.1.33",
|
|
26
|
+
"bopodev-contracts": "0.1.33"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@types/archiver": "^7.0.0",
|
|
29
30
|
"@types/cors": "^2.8.19",
|
|
30
31
|
"@types/express": "^5.0.3",
|
|
31
32
|
"@types/multer": "^2.1.0",
|
|
33
|
+
"@types/turndown": "^5.0.6",
|
|
32
34
|
"@types/ws": "^8.18.1",
|
|
33
35
|
"tsx": "^4.20.5"
|
|
34
36
|
},
|
package/src/lib/agent-config.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AgentRuntimeConfigSchema,
|
|
3
|
+
companySkillAllowlistOnly,
|
|
4
|
+
type AgentRuntimeConfig,
|
|
5
|
+
type ThinkingEffort
|
|
6
|
+
} from "bopodev-contracts";
|
|
2
7
|
import { normalizeAbsolutePath } from "./instance-paths";
|
|
3
8
|
|
|
4
9
|
export type LegacyRuntimeFields = {
|
|
@@ -16,6 +21,8 @@ export type LegacyRuntimeFields = {
|
|
|
16
21
|
allowWebSearch?: boolean;
|
|
17
22
|
};
|
|
18
23
|
runtimeEnv?: Record<string, string>;
|
|
24
|
+
/** Company skill ids only (bundled ids are stripped on normalize). */
|
|
25
|
+
enabledSkillIds?: string[] | null;
|
|
19
26
|
};
|
|
20
27
|
|
|
21
28
|
export type NormalizedRuntimeConfig = {
|
|
@@ -26,6 +33,8 @@ export type NormalizedRuntimeConfig = {
|
|
|
26
33
|
runtimeModel?: string;
|
|
27
34
|
runtimeThinkingEffort: ThinkingEffort;
|
|
28
35
|
bootstrapPrompt?: string;
|
|
36
|
+
/** Omitted = inject all skills (legacy). Present = company skill ids only; bundled skills are always injected. */
|
|
37
|
+
enabledSkillIds?: string[];
|
|
29
38
|
runtimeTimeoutSec: number;
|
|
30
39
|
interruptGraceSec: number;
|
|
31
40
|
runPolicy: {
|
|
@@ -109,6 +118,9 @@ export function normalizeRuntimeConfig(input: {
|
|
|
109
118
|
if (legacy.runPolicy !== undefined) {
|
|
110
119
|
merged.runPolicy = legacy.runPolicy;
|
|
111
120
|
}
|
|
121
|
+
if (legacy.enabledSkillIds !== undefined) {
|
|
122
|
+
merged.enabledSkillIds = legacy.enabledSkillIds;
|
|
123
|
+
}
|
|
112
124
|
|
|
113
125
|
const parsed = AgentRuntimeConfigSchema.partial().parse({
|
|
114
126
|
...merged,
|
|
@@ -131,7 +143,11 @@ export function normalizeRuntimeConfig(input: {
|
|
|
131
143
|
runPolicy: {
|
|
132
144
|
sandboxMode: parsed.runPolicy?.sandboxMode ?? "workspace_write",
|
|
133
145
|
allowWebSearch: parsed.runPolicy?.allowWebSearch ?? false
|
|
134
|
-
}
|
|
146
|
+
},
|
|
147
|
+
enabledSkillIds:
|
|
148
|
+
parsed.enabledSkillIds === null || parsed.enabledSkillIds === undefined
|
|
149
|
+
? undefined
|
|
150
|
+
: companySkillAllowlistOnly(parsed.enabledSkillIds)
|
|
135
151
|
};
|
|
136
152
|
}
|
|
137
153
|
|
|
@@ -165,6 +181,8 @@ export function parseRuntimeConfigFromAgentRow(agent: Record<string, unknown>):
|
|
|
165
181
|
runtimeModel,
|
|
166
182
|
runtimeThinkingEffort: parseThinkingEffort(agent.runtimeThinkingEffort),
|
|
167
183
|
bootstrapPrompt: toText(agent.bootstrapPrompt),
|
|
184
|
+
enabledSkillIds:
|
|
185
|
+
fallback.enabledSkillIds === undefined ? undefined : companySkillAllowlistOnly(fallback.enabledSkillIds),
|
|
168
186
|
runtimeTimeoutSec: Math.max(0, timeoutSec),
|
|
169
187
|
interruptGraceSec: Math.max(0, toNumber(agent.interruptGraceSec) ?? 15),
|
|
170
188
|
runPolicy
|
|
@@ -187,14 +205,18 @@ export function runtimeConfigToDb(runtime: NormalizedRuntimeConfig) {
|
|
|
187
205
|
}
|
|
188
206
|
|
|
189
207
|
export function runtimeConfigToStateBlobPatch(runtime: NormalizedRuntimeConfig) {
|
|
208
|
+
const runtimePatch: Record<string, unknown> = {
|
|
209
|
+
command: runtime.runtimeCommand,
|
|
210
|
+
args: runtime.runtimeArgs,
|
|
211
|
+
cwd: runtime.runtimeCwd,
|
|
212
|
+
env: runtime.runtimeEnv,
|
|
213
|
+
timeoutMs: runtime.runtimeTimeoutSec > 0 ? runtime.runtimeTimeoutSec * 1000 : undefined
|
|
214
|
+
};
|
|
215
|
+
if (runtime.enabledSkillIds !== undefined) {
|
|
216
|
+
runtimePatch.enabledSkillIds = runtime.enabledSkillIds;
|
|
217
|
+
}
|
|
190
218
|
return {
|
|
191
|
-
runtime:
|
|
192
|
-
command: runtime.runtimeCommand,
|
|
193
|
-
args: runtime.runtimeArgs,
|
|
194
|
-
cwd: runtime.runtimeCwd,
|
|
195
|
-
env: runtime.runtimeEnv,
|
|
196
|
-
timeoutMs: runtime.runtimeTimeoutSec > 0 ? runtime.runtimeTimeoutSec * 1000 : undefined
|
|
197
|
-
},
|
|
219
|
+
runtime: runtimePatch,
|
|
198
220
|
promptTemplate: runtime.bootstrapPrompt
|
|
199
221
|
};
|
|
200
222
|
}
|
|
@@ -208,6 +230,7 @@ function parseRuntimeFromStateBlob(raw: unknown) {
|
|
|
208
230
|
env?: Record<string, string>;
|
|
209
231
|
model?: string;
|
|
210
232
|
timeoutMs?: number;
|
|
233
|
+
enabledSkillIds?: string[];
|
|
211
234
|
};
|
|
212
235
|
}
|
|
213
236
|
try {
|
|
@@ -219,6 +242,7 @@ function parseRuntimeFromStateBlob(raw: unknown) {
|
|
|
219
242
|
env?: unknown;
|
|
220
243
|
model?: unknown;
|
|
221
244
|
timeoutMs?: unknown;
|
|
245
|
+
enabledSkillIds?: unknown;
|
|
222
246
|
};
|
|
223
247
|
};
|
|
224
248
|
const runtime = parsed.runtime ?? {};
|
|
@@ -228,13 +252,37 @@ function parseRuntimeFromStateBlob(raw: unknown) {
|
|
|
228
252
|
cwd: typeof runtime.cwd === "string" ? runtime.cwd : undefined,
|
|
229
253
|
env: toRecord(runtime.env),
|
|
230
254
|
model: typeof runtime.model === "string" && runtime.model.trim().length > 0 ? runtime.model.trim() : undefined,
|
|
231
|
-
timeoutMs: toNumber(runtime.timeoutMs)
|
|
255
|
+
timeoutMs: toNumber(runtime.timeoutMs),
|
|
256
|
+
enabledSkillIds: parseEnabledSkillIdsFromState(runtime.enabledSkillIds)
|
|
232
257
|
};
|
|
233
258
|
} catch {
|
|
234
259
|
return {};
|
|
235
260
|
}
|
|
236
261
|
}
|
|
237
262
|
|
|
263
|
+
function parseEnabledSkillIdsFromState(raw: unknown): string[] | undefined {
|
|
264
|
+
if (!Array.isArray(raw)) {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
const out: string[] = [];
|
|
268
|
+
const seen = new Set<string>();
|
|
269
|
+
for (const entry of raw) {
|
|
270
|
+
if (typeof entry !== "string") {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const trimmed = entry.trim();
|
|
274
|
+
if (!trimmed || seen.has(trimmed)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
seen.add(trimmed);
|
|
278
|
+
out.push(trimmed);
|
|
279
|
+
if (out.length >= 64) {
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
238
286
|
function parseStringArray(raw: unknown) {
|
|
239
287
|
if (typeof raw !== "string") {
|
|
240
288
|
return null;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bopodev-control-plane
|
|
3
|
+
description: >
|
|
4
|
+
Coordinate heartbeat work through a control-plane API: assignments, checkout,
|
|
5
|
+
comments, status updates, approvals, and delegation. Use this for orchestration
|
|
6
|
+
only, not domain implementation itself.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# BopoDev Control Plane Skill
|
|
10
|
+
|
|
11
|
+
Use this skill when the agent must interact with the control plane for issue
|
|
12
|
+
coordination (assignment management, workflow state, and delegation).
|
|
13
|
+
|
|
14
|
+
Do not use it for coding implementation details; use normal local tooling for that.
|
|
15
|
+
|
|
16
|
+
## Required context
|
|
17
|
+
|
|
18
|
+
Expected env variables:
|
|
19
|
+
|
|
20
|
+
- `BOPODEV_AGENT_ID`
|
|
21
|
+
- `BOPODEV_COMPANY_ID`
|
|
22
|
+
- `BOPODEV_RUN_ID`
|
|
23
|
+
- `BOPODEV_API_BASE_URL`
|
|
24
|
+
- `BOPODEV_REQUEST_HEADERS_JSON` (fallback JSON map for required request headers)
|
|
25
|
+
- `BOPODEV_ACTOR_TYPE`
|
|
26
|
+
- `BOPODEV_ACTOR_ID`
|
|
27
|
+
- `BOPODEV_ACTOR_COMPANIES`
|
|
28
|
+
- `BOPODEV_ACTOR_PERMISSIONS`
|
|
29
|
+
|
|
30
|
+
Wake context (optional):
|
|
31
|
+
|
|
32
|
+
- `BOPODEV_TASK_ID`
|
|
33
|
+
- `BOPODEV_WAKE_REASON`
|
|
34
|
+
- `BOPODEV_WAKE_COMMENT_ID`
|
|
35
|
+
- `BOPODEV_APPROVAL_ID`
|
|
36
|
+
- `BOPODEV_APPROVAL_STATUS`
|
|
37
|
+
- `BOPODEV_LINKED_ISSUE_IDS`
|
|
38
|
+
|
|
39
|
+
If control-plane connectivity is unavailable, do not attempt control-plane mutations.
|
|
40
|
+
Fail fast, report the connectivity gap once with the exact error, and avoid repeated retries in the same heartbeat run.
|
|
41
|
+
|
|
42
|
+
## Heartbeat procedure
|
|
43
|
+
|
|
44
|
+
1. Resolve identity from env: `BOPODEV_AGENT_ID`, `BOPODEV_COMPANY_ID`, `BOPODEV_RUN_ID`.
|
|
45
|
+
2. If approval-related wake context exists (env or heartbeat prompt), process linked approvals first.
|
|
46
|
+
3. Use assigned issues from heartbeat prompt as primary work queue.
|
|
47
|
+
4. Prioritize `in_progress`, then `todo`; only revisit `blocked` with new context.
|
|
48
|
+
5. Read issue comments for current context before mutating status.
|
|
49
|
+
6. Do the work.
|
|
50
|
+
7. Publish progress and update final state (`done`, `blocked`, `in_review`).
|
|
51
|
+
8. Delegate through subtasks when decomposition is needed.
|
|
52
|
+
|
|
53
|
+
## API usage pattern
|
|
54
|
+
|
|
55
|
+
All API routes are rooted at `BOPODEV_API_BASE_URL` (no `/api` prefix in this project).
|
|
56
|
+
|
|
57
|
+
Use direct env vars for request headers (preferred, deterministic):
|
|
58
|
+
|
|
59
|
+
- `x-company-id`
|
|
60
|
+
- `x-actor-type`
|
|
61
|
+
- `x-actor-id`
|
|
62
|
+
- `x-actor-companies`
|
|
63
|
+
- `x-actor-permissions`
|
|
64
|
+
|
|
65
|
+
Recommended curl header pattern (do not parse JSON first):
|
|
66
|
+
|
|
67
|
+
`curl -sS -H "x-company-id: $BOPODEV_COMPANY_ID" -H "x-actor-type: $BOPODEV_ACTOR_TYPE" -H "x-actor-id: $BOPODEV_ACTOR_ID" -H "x-actor-companies: $BOPODEV_ACTOR_COMPANIES" -H "x-actor-permissions: $BOPODEV_ACTOR_PERMISSIONS" ...`
|
|
68
|
+
|
|
69
|
+
Only use `BOPODEV_REQUEST_HEADERS_JSON` as compatibility fallback when direct vars are unavailable.
|
|
70
|
+
|
|
71
|
+
Prefer direct header flags from env when scripting requests. Do not assume `python` is installed.
|
|
72
|
+
If you need a JSON request body, write it to a temp file or heredoc and use `curl --data @file`
|
|
73
|
+
instead of hand-escaping multiline JSON in the shell.
|
|
74
|
+
The runtime shell is `zsh` on macOS. Avoid Bash-only features such as `local -n`,
|
|
75
|
+
`declare -n`, `mapfile`, and `readarray`.
|
|
76
|
+
|
|
77
|
+
When creating hires, set `requestApproval: true` by default (board-level bypass should be rare and explicit).
|
|
78
|
+
|
|
79
|
+
## Critical safety rules
|
|
80
|
+
|
|
81
|
+
- Heartbeat-assigned issues may already be claimed by the current run. Do not call a
|
|
82
|
+
nonexistent checkout endpoint in this project.
|
|
83
|
+
- Never assume `POST /issues/{issueId}/checkout` exists here.
|
|
84
|
+
- Never assume `GET /agents/{agentId}` exists here.
|
|
85
|
+
- Never retry ownership conflicts (`409`).
|
|
86
|
+
- Never self-assign random work outside assignment/mention handoff rules.
|
|
87
|
+
- Always leave a useful progress or blocker comment before heartbeat exit.
|
|
88
|
+
- If blocked, update state to `blocked` and include a specific unblock path.
|
|
89
|
+
- Do not repeatedly post duplicate blocked comments when nothing changed.
|
|
90
|
+
- Escalate through reporting chain for cross-team blockers.
|
|
91
|
+
- Do not loop on repeated `curl` retries for the same failing endpoint in one run; include one precise failure message and exit.
|
|
92
|
+
|
|
93
|
+
## Comment style
|
|
94
|
+
|
|
95
|
+
When adding comments, use concise markdown:
|
|
96
|
+
|
|
97
|
+
- one short status line
|
|
98
|
+
- bullet list of what changed or what is blocked
|
|
99
|
+
- links to related entities when available (issue/approval/agent/run)
|
|
100
|
+
|
|
101
|
+
## Quick endpoint reference
|
|
102
|
+
|
|
103
|
+
| Action | Endpoint |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| List agents | `GET /agents` |
|
|
106
|
+
| Update agent | `PUT /agents/{agentId}` |
|
|
107
|
+
| Hire request (approval-backed) | `POST /agents` with `requestApproval: true` |
|
|
108
|
+
| List approvals | `GET /governance/approvals` |
|
|
109
|
+
| Read issue comments | `GET /issues/{issueId}/comments` |
|
|
110
|
+
| Add issue comment | `POST /issues/{issueId}/comments` |
|
|
111
|
+
| Update issue | `PUT /issues/{issueId}` |
|
|
112
|
+
| Create subtask issue | `POST /issues` |
|
|
113
|
+
|
|
114
|
+
## Important route notes
|
|
115
|
+
|
|
116
|
+
- To inspect your own agent, use `GET /agents` and filter by id locally.
|
|
117
|
+
- `GET /agents` returns a wrapped envelope: `{ "ok": true, "data": [...] }`.
|
|
118
|
+
- Treat any non-envelope shape as a hard failure for this run.
|
|
119
|
+
- Recommended deterministic filter:
|
|
120
|
+
`jq -er --arg id "$BOPODEV_AGENT_ID" '.data | if type=="array" then . else error("invalid_agents_payload") end | map(select((.id? // "") == $id)) | .[0]'`
|
|
121
|
+
- Heartbeat runs already claim their assigned issues; move status with `PUT /issues/{issueId}`.
|
|
122
|
+
- For bootstrap prompt updates, prefer a top-level `bootstrapPrompt` field unless you need
|
|
123
|
+
other runtime settings in `runtimeConfig`.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bopodev-create-agent
|
|
3
|
+
description: >
|
|
4
|
+
Hire agents through governance-aware control-plane flow: inspect adapter
|
|
5
|
+
options, draft configuration, and submit approval-backed hire requests.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# BopoDev Create Agent Skill
|
|
9
|
+
|
|
10
|
+
Use this skill when creating or revising agent hires in the control plane.
|
|
11
|
+
|
|
12
|
+
## Preconditions
|
|
13
|
+
|
|
14
|
+
One of the following must be true:
|
|
15
|
+
|
|
16
|
+
- caller has board access, or
|
|
17
|
+
- caller has agent creation permission.
|
|
18
|
+
|
|
19
|
+
If permission is missing, escalate to a manager/board actor.
|
|
20
|
+
|
|
21
|
+
## Standard workflow
|
|
22
|
+
|
|
23
|
+
1. Confirm identity and active company context.
|
|
24
|
+
2. Compare existing agent configurations from `GET /agents` for reusable patterns.
|
|
25
|
+
3. Choose role, provider, reporting line, and runtime heartbeat profile.
|
|
26
|
+
4. Draft agent prompt/instructions with role-scoped responsibilities.
|
|
27
|
+
5. Set `capabilities` (required for every hire): a short plain-language line for the org chart and heartbeat team roster—what this agent does for delegation. If the request came from a delegated hiring issue, prefer `delegationIntent.requestedCapabilities` or the issue metadata `delegatedHiringIntent.requestedCapabilities` when present; otherwise write one from the role and brief.
|
|
28
|
+
6. Submit hire request and capture approval linkage.
|
|
29
|
+
7. Track approval state and post follow-up comments with links.
|
|
30
|
+
8. On approval wake, close or update linked issues accordingly.
|
|
31
|
+
|
|
32
|
+
## Payload checklist
|
|
33
|
+
|
|
34
|
+
Before submission, ensure payload includes:
|
|
35
|
+
|
|
36
|
+
- `name`
|
|
37
|
+
- `role`
|
|
38
|
+
- `providerType`
|
|
39
|
+
- `heartbeatCron`
|
|
40
|
+
- `monthlyBudgetUsd`
|
|
41
|
+
- optional `managerAgentId`
|
|
42
|
+
- optional `canHireAgents`
|
|
43
|
+
- `capabilities` (short description for org chart and team roster; include on every hire)
|
|
44
|
+
- optional `bootstrapPrompt` (extra standing instructions only; operating docs are injected via heartbeat env) or supported `runtimeConfig`
|
|
45
|
+
- `requestApproval` (defaults to `true`; keep `true` for routine hires)
|
|
46
|
+
|
|
47
|
+
Do not use unsupported fields such as:
|
|
48
|
+
|
|
49
|
+
- `adapterType`
|
|
50
|
+
- `adapterConfig`
|
|
51
|
+
- `reportsTo`
|
|
52
|
+
- arbitrary nested `runtimeConfig` keys outside the supported runtime contract
|
|
53
|
+
|
|
54
|
+
## Minimal approved shape
|
|
55
|
+
|
|
56
|
+
For a Codex hire, prefer this shape:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"name": "Founding Engineer",
|
|
61
|
+
"role": "Founding Engineer",
|
|
62
|
+
"capabilities": "Ships product changes with tests, clear handoffs, and accurate issue updates.",
|
|
63
|
+
"providerType": "codex",
|
|
64
|
+
"managerAgentId": "<manager-agent-id>",
|
|
65
|
+
"heartbeatCron": "*/5 * * * *",
|
|
66
|
+
"monthlyBudgetUsd": 100,
|
|
67
|
+
"bootstrapPrompt": "Optional: prefer small PRs and note blockers in employee_comment.",
|
|
68
|
+
"requestApproval": true
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If you need multiline prompt text, write the JSON to a temp file or heredoc and submit with
|
|
73
|
+
`curl --data @file` instead of shell-escaping the body inline.
|
|
74
|
+
The runtime shell is `zsh` on macOS, so keep helper scripts POSIX/zsh-compatible and avoid
|
|
75
|
+
Bash-only features like `local -n`, `declare -n`, `mapfile`, and `readarray`.
|
|
76
|
+
|
|
77
|
+
## Governance rules
|
|
78
|
+
|
|
79
|
+
- Use approval-backed hiring by default.
|
|
80
|
+
- Do not bypass governance for routine hires (only board-level operators should bypass intentionally).
|
|
81
|
+
- If board feedback requests revisions, resubmit with explicit deltas.
|
|
82
|
+
- Keep approval threads updated with issue/agent/approval links.
|
|
83
|
+
|
|
84
|
+
## Quality bar
|
|
85
|
+
|
|
86
|
+
- Prefer proven adapter/runtime templates over one-off configs.
|
|
87
|
+
- Keep prompts operational and bounded to the role.
|
|
88
|
+
- Do not store plaintext secrets unless strictly required.
|
|
89
|
+
- Validate reporting chain and company ownership before submission.
|
|
90
|
+
- Use deterministic, auditable language in approval comments.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { BUILTIN_BOPO_SKILL_IDS } from "bopodev-contracts";
|
|
5
|
+
|
|
6
|
+
const DIR = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
export type BuiltinBopoSkill = {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
content: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const BUILTIN_TITLES: Record<string, string> = {
|
|
15
|
+
"bopodev-control-plane": "Bopo control plane",
|
|
16
|
+
"bopodev-create-agent": "Bopo create agent",
|
|
17
|
+
"para-memory-files": "PARA memory files"
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function readBundled(id: string, title: string): BuiltinBopoSkill {
|
|
21
|
+
try {
|
|
22
|
+
const content = readFileSync(join(DIR, `${id}.md`), "utf8");
|
|
23
|
+
return { id, title, content };
|
|
24
|
+
} catch {
|
|
25
|
+
return {
|
|
26
|
+
id,
|
|
27
|
+
title,
|
|
28
|
+
content: `# ${title}\n\nBuilt-in skill text is not available in this build (missing bundled copy of \`${id}.md\`).\n`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Injected into local agent runtimes alongside company `skills/`. Read-only in Settings UI. */
|
|
34
|
+
export const BUILTIN_BOPO_SKILLS: BuiltinBopoSkill[] = BUILTIN_BOPO_SKILL_IDS.map((id) =>
|
|
35
|
+
readBundled(id, BUILTIN_TITLES[id] ?? id)
|
|
36
|
+
);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: para-memory-files
|
|
3
|
+
description: >
|
|
4
|
+
File-backed memory using PARA (Projects, Areas, Resources, Archives). Use for
|
|
5
|
+
persistent recall across sessions: durable facts, daily notes, and user habits.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# PARA Memory Files
|
|
9
|
+
|
|
10
|
+
Use this skill whenever context must survive beyond the current runtime session.
|
|
11
|
+
|
|
12
|
+
## Memory layers
|
|
13
|
+
|
|
14
|
+
1. Knowledge graph (`life/`)
|
|
15
|
+
- entity folders with `summary.md` and `items.yaml`
|
|
16
|
+
- durable, queryable facts
|
|
17
|
+
2. Daily notes (`memory/YYYY-MM-DD.md`)
|
|
18
|
+
- chronological event log
|
|
19
|
+
- temporary observations before curation
|
|
20
|
+
3. Tacit memory (`MEMORY.md`)
|
|
21
|
+
- user preferences, work style, collaboration patterns
|
|
22
|
+
|
|
23
|
+
## PARA organization
|
|
24
|
+
|
|
25
|
+
- `projects/`: active efforts with goals/deadlines
|
|
26
|
+
- `areas/`: ongoing responsibilities
|
|
27
|
+
- `resources/`: reusable reference knowledge
|
|
28
|
+
- `archives/`: inactive entities moved from other buckets
|
|
29
|
+
|
|
30
|
+
## Operating rules
|
|
31
|
+
|
|
32
|
+
- Write durable facts immediately to `items.yaml`.
|
|
33
|
+
- Keep `summary.md` short and regenerate from active facts.
|
|
34
|
+
- Never delete facts; supersede with status and replacement reference.
|
|
35
|
+
- Move inactive entities to `archives` rather than removing them.
|
|
36
|
+
- Prefer writing to disk over relying on transient model context.
|
|
37
|
+
|
|
38
|
+
## Recall workflow
|
|
39
|
+
|
|
40
|
+
1. Capture raw event in daily note.
|
|
41
|
+
2. Promote durable facts into entity files.
|
|
42
|
+
3. Update entity summary from durable facts.
|
|
43
|
+
4. Update tacit memory when user operating patterns become clear.
|
|
44
|
+
|
|
45
|
+
## Planning notes
|
|
46
|
+
|
|
47
|
+
- Store shared plans in project `plans/` where collaborators can access them.
|
|
48
|
+
- Mark superseded plans to prevent stale guidance drift.
|
|
@@ -61,6 +61,11 @@ export function resolveCompanyProjectsWorkspacePath(companyId: string) {
|
|
|
61
61
|
return join(resolveBopoInstanceRoot(), "workspaces", safeCompanyId);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Company-managed runtime skills (`skills/<id>/SKILL.md`), exportable with company zip. */
|
|
65
|
+
export function resolveCompanySkillsPath(companyId: string) {
|
|
66
|
+
return join(resolveCompanyProjectsWorkspacePath(companyId), "skills");
|
|
67
|
+
}
|
|
68
|
+
|
|
64
69
|
export function resolveAgentFallbackWorkspacePath(companyId: string, agentId: string) {
|
|
65
70
|
const safeCompanyId = assertPathSegment(companyId, "companyId");
|
|
66
71
|
const safeAgentId = assertPathSegment(agentId, "agentId");
|
package/src/routes/agents.ts
CHANGED
|
@@ -62,7 +62,8 @@ const legacyRuntimeConfigSchema = z.object({
|
|
|
62
62
|
sandboxMode: z.enum(["workspace_write", "full_access"]).optional(),
|
|
63
63
|
allowWebSearch: z.boolean().optional()
|
|
64
64
|
})
|
|
65
|
-
.optional()
|
|
65
|
+
.optional(),
|
|
66
|
+
enabledSkillIds: z.array(z.string().min(1)).max(64).nullable().optional()
|
|
66
67
|
});
|
|
67
68
|
|
|
68
69
|
const createAgentSchema = AgentCreateRequestSchema.extend({
|
|
@@ -118,7 +119,8 @@ const UPDATE_AGENT_ALLOWED_KEYS = new Set([
|
|
|
118
119
|
"runtimeTimeoutSec",
|
|
119
120
|
"interruptGraceSec",
|
|
120
121
|
"runtimeEnv",
|
|
121
|
-
"runPolicy"
|
|
122
|
+
"runPolicy",
|
|
123
|
+
"enabledSkillIds"
|
|
122
124
|
]);
|
|
123
125
|
const UPDATE_RUNTIME_CONFIG_ALLOWED_KEYS = new Set([
|
|
124
126
|
"runtimeCommand",
|
|
@@ -130,15 +132,18 @@ const UPDATE_RUNTIME_CONFIG_ALLOWED_KEYS = new Set([
|
|
|
130
132
|
"bootstrapPrompt",
|
|
131
133
|
"runtimeTimeoutSec",
|
|
132
134
|
"interruptGraceSec",
|
|
133
|
-
"runPolicy"
|
|
135
|
+
"runPolicy",
|
|
136
|
+
"enabledSkillIds"
|
|
134
137
|
]);
|
|
135
138
|
|
|
136
139
|
function toAgentResponse(agent: Record<string, unknown>) {
|
|
140
|
+
const rt = parseRuntimeConfigFromAgentRow(agent);
|
|
137
141
|
return {
|
|
138
142
|
...agent,
|
|
139
143
|
monthlyBudgetUsd:
|
|
140
144
|
typeof agent.monthlyBudgetUsd === "number" ? agent.monthlyBudgetUsd : Number(agent.monthlyBudgetUsd ?? 0),
|
|
141
|
-
usedBudgetUsd: typeof agent.usedBudgetUsd === "number" ? agent.usedBudgetUsd : Number(agent.usedBudgetUsd ?? 0)
|
|
145
|
+
usedBudgetUsd: typeof agent.usedBudgetUsd === "number" ? agent.usedBudgetUsd : Number(agent.usedBudgetUsd ?? 0),
|
|
146
|
+
enabledSkillIds: rt.enabledSkillIds === undefined ? null : rt.enabledSkillIds
|
|
142
147
|
};
|
|
143
148
|
}
|
|
144
149
|
|
|
@@ -400,7 +405,8 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
400
405
|
runtimeTimeoutSec: parsed.data.runtimeTimeoutSec,
|
|
401
406
|
interruptGraceSec: parsed.data.interruptGraceSec,
|
|
402
407
|
runtimeEnv: parsed.data.runtimeEnv,
|
|
403
|
-
runPolicy: parsed.data.runPolicy
|
|
408
|
+
runPolicy: parsed.data.runPolicy,
|
|
409
|
+
enabledSkillIds: parsed.data.enabledSkillIds
|
|
404
410
|
},
|
|
405
411
|
defaultRuntimeCwd
|
|
406
412
|
});
|
|
@@ -408,6 +414,12 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
408
414
|
} catch (error) {
|
|
409
415
|
return sendError(res, String(error), 422);
|
|
410
416
|
}
|
|
417
|
+
const rc = parsed.data.runtimeConfig;
|
|
418
|
+
const hasEnabledSkillIdsKey =
|
|
419
|
+
rc !== undefined && rc !== null && typeof rc === "object" && "enabledSkillIds" in rc;
|
|
420
|
+
if (!hasEnabledSkillIdsKey && parsed.data.enabledSkillIds === undefined) {
|
|
421
|
+
runtimeConfig = { ...runtimeConfig, enabledSkillIds: [] };
|
|
422
|
+
}
|
|
411
423
|
runtimeConfig.runtimeModel = await resolveOpencodeRuntimeModel(parsed.data.providerType, runtimeConfig);
|
|
412
424
|
runtimeConfig.runtimeModel = resolveRuntimeModelForProvider(parsed.data.providerType, runtimeConfig.runtimeModel);
|
|
413
425
|
if (!ensureNamedRuntimeModel(parsed.data.providerType, runtimeConfig.runtimeModel)) {
|
|
@@ -560,7 +572,8 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
560
572
|
parsed.data.runtimeTimeoutSec !== undefined ||
|
|
561
573
|
parsed.data.interruptGraceSec !== undefined ||
|
|
562
574
|
parsed.data.runtimeEnv !== undefined ||
|
|
563
|
-
parsed.data.runPolicy !== undefined
|
|
575
|
+
parsed.data.runPolicy !== undefined ||
|
|
576
|
+
parsed.data.enabledSkillIds !== undefined;
|
|
564
577
|
try {
|
|
565
578
|
let nextRuntime = {
|
|
566
579
|
...existingRuntime,
|
|
@@ -581,7 +594,8 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
581
594
|
runtimeTimeoutSec: parsed.data.runtimeTimeoutSec ?? existingRuntime.runtimeTimeoutSec,
|
|
582
595
|
interruptGraceSec: parsed.data.interruptGraceSec,
|
|
583
596
|
runtimeEnv: parsed.data.runtimeEnv,
|
|
584
|
-
runPolicy: parsed.data.runPolicy
|
|
597
|
+
runPolicy: parsed.data.runPolicy,
|
|
598
|
+
enabledSkillIds: parsed.data.enabledSkillIds
|
|
585
599
|
}
|
|
586
600
|
})
|
|
587
601
|
: {})
|