@monster-orange/den 0.4.12 → 0.4.14
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/CHANGELOG.md +151 -0
- package/README.md +7 -7
- package/docs/distributions.md +189 -11
- package/package.json +1 -1
- package/src/shared/agents/conversations.ts +227 -0
- package/src/shared/agents/default-pack/be-dev.md +1 -0
- package/src/shared/agents/default-pack/devops.md +1 -0
- package/src/shared/agents/default-pack/fe-dev.md +1 -0
- package/src/shared/agents/default-pack/orchestrator.md +3 -1
- package/src/shared/agents/default-pack/planner.md +1 -0
- package/src/shared/agents/default-pack/reviewer.md +1 -0
- package/src/shared/agents/default-pack/tester.md +16 -0
- package/src/shared/agents/default-pack/ux-designer.md +1 -0
- package/src/shared/agents/den-claude.ts +128 -13
- package/src/shared/agents/permissions.ts +108 -0
- package/src/shared/agents/persona-store.ts +286 -0
- package/src/shared/agents/personas.ts +116 -12
- package/src/shared/agents/records.ts +33 -0
- package/src/shared/agents/roles.ts +288 -0
- package/src/shared/agents/runtime.ts +50 -0
- package/src/shared/den-config.ts +22 -1
- package/src/shared/environment-manager/service-runner.ts +31 -12
- package/src/shared/environment-manager/workspaces.ts +11 -0
- package/src/ui/cli/agent-ops.ts +796 -29
- package/src/ui/cli/agents-command.ts +97 -3
- package/src/ui/cli/env-probe.ts +4 -1
- package/src/ui/mcp/api.ts +12 -1
- package/src/ui/mcp/tools.ts +60 -1
- package/src/ui/server/agent-resume-grant.ts +35 -0
- package/src/ui/server/agent-routes.ts +412 -22
- package/src/ui/server/public/agent-start.js +3 -2
- package/src/ui/server/public/agents-page.js +693 -49
- package/src/ui/server/public/agents.html +76 -15
- package/src/ui/server/public/i18n/es.js +143 -0
- package/src/ui/server/public/i18n/ja.js +144 -0
- package/src/ui/server/public/persona-ui.css +563 -0
- package/src/ui/server/public/persona-ui.js +796 -0
- package/src/ui/server/public/settings-page.js +4 -9
- package/src/ui/server/public/settings-registry.js +22 -0
- package/src/ui/server/public/settings.html +1 -0
- package/src/ui/server/public/shell.js +140 -0
- package/src/ui/server/public/topbar.js +2 -2
- package/src/ui/server/request-guard.ts +17 -4
- package/src/ui/server/route-classes.ts +31 -0
- package/src/ui/server/session-routes.ts +21 -5
- package/src/ui/server/ui-assets.ts +3 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roles and persona categories (AG-10.5, AG-11): two small registries.
|
|
3
|
+
*
|
|
4
|
+
* A role is a template and a dispatch key. Den owns planner, orchestrator,
|
|
5
|
+
* sde, reviewer, tester and designer. Each has default instructions, a
|
|
6
|
+
* permission mode and a command preset (plain command prefixes the agent
|
|
7
|
+
* may run without asking). Picking a role in New persona fills the form in
|
|
8
|
+
* (never over text already there), and the role adds nothing hidden to an
|
|
9
|
+
* agent's prompt: what it receives is the persona's instructions.
|
|
10
|
+
*
|
|
11
|
+
* A distribution adds roles in its config (`agents.roles`), and may extend
|
|
12
|
+
* a Den role's preset there, never redefine it:
|
|
13
|
+
*
|
|
14
|
+
* agents:
|
|
15
|
+
* roles:
|
|
16
|
+
* copywriter: { description: …, instructions: …, mode: ask, preset: [], category: marketing }
|
|
17
|
+
* reviewer: { preset: [semgrep] } # adds to Den's preset only
|
|
18
|
+
*
|
|
19
|
+
* Categories group personas in the Start list and Settings › Personas.
|
|
20
|
+
* Den's are Engineering, Design, Product, Marketing and Operations; a
|
|
21
|
+
* distribution adds more (`agents.categories: [{ id, label }]`).
|
|
22
|
+
*/
|
|
23
|
+
import { checkCommands, PERMISSION_INTENTS } from "./permissions.js";
|
|
24
|
+
import type { PermissionIntent } from "./permissions.js";
|
|
25
|
+
|
|
26
|
+
/** Where a role or category came from (hub: later, once Den Hub exists). */
|
|
27
|
+
export type RegistrySource = "den" | "distribution" | "project" | "hub";
|
|
28
|
+
|
|
29
|
+
export interface RoleTemplate {
|
|
30
|
+
id: string;
|
|
31
|
+
source: RegistrySource;
|
|
32
|
+
/** One line: what a persona of this role does. */
|
|
33
|
+
description: string;
|
|
34
|
+
/** The starting instructions New persona fills in. */
|
|
35
|
+
instructions: string;
|
|
36
|
+
/** The starting permission mode. */
|
|
37
|
+
mode: PermissionIntent;
|
|
38
|
+
/** Plain command prefixes allowed without asking (AG-13.3). */
|
|
39
|
+
preset: string[];
|
|
40
|
+
/** The category a new persona of this role starts in. */
|
|
41
|
+
category: string;
|
|
42
|
+
/** Set when a distribution extended a Den role's preset. */
|
|
43
|
+
extendedBy?: RegistrySource;
|
|
44
|
+
/** The commands that extension added (shown apart from Den's). */
|
|
45
|
+
extension?: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Category {
|
|
49
|
+
id: string;
|
|
50
|
+
label: string;
|
|
51
|
+
source: RegistrySource;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A role or category id: lowercase letters, digits and "-", 1-32 long. */
|
|
55
|
+
export const REGISTRY_ID_RE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
56
|
+
|
|
57
|
+
/** Den's categories, in the order lists show them. */
|
|
58
|
+
export const DEN_CATEGORIES: readonly Category[] = [
|
|
59
|
+
{ id: "engineering", label: "Engineering", source: "den" },
|
|
60
|
+
{ id: "design", label: "Design", source: "den" },
|
|
61
|
+
{ id: "product", label: "Product", source: "den" },
|
|
62
|
+
{ id: "marketing", label: "Marketing", source: "den" },
|
|
63
|
+
{ id: "operations", label: "Operations", source: "den" },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Den's roles. The presets are read-only commands only: a preset is
|
|
68
|
+
* allowed without asking, so Den's defaults never let an agent change
|
|
69
|
+
* anything unasked.
|
|
70
|
+
*/
|
|
71
|
+
export const DEN_ROLES: readonly RoleTemplate[] = [
|
|
72
|
+
{
|
|
73
|
+
id: "planner",
|
|
74
|
+
source: "den",
|
|
75
|
+
description:
|
|
76
|
+
"Turns goals into small, ready stories with acceptance criteria.",
|
|
77
|
+
instructions:
|
|
78
|
+
"Read the workspace's specs and open work items. Split goals into stories small enough for one pull request, each with testable acceptance criteria. Hand ready stories to the orchestrator. Don't write code.",
|
|
79
|
+
mode: "ask",
|
|
80
|
+
preset: ["gh issue list", "gh issue view"],
|
|
81
|
+
category: "engineering",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "orchestrator",
|
|
85
|
+
source: "den",
|
|
86
|
+
description:
|
|
87
|
+
"Hands ready stories to other agents and follows them to done.",
|
|
88
|
+
instructions:
|
|
89
|
+
"Take the next ready story in priority order and start the persona that fits it, one agent per story. Respect the workspace's concurrency cap. Ask a person before anything irreversible; never approve or merge.",
|
|
90
|
+
mode: "ask",
|
|
91
|
+
preset: ["gh issue list", "gh pr view"],
|
|
92
|
+
category: "engineering",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "sde",
|
|
96
|
+
source: "den",
|
|
97
|
+
description: "Implements one story, test first, and opens a pull request.",
|
|
98
|
+
instructions:
|
|
99
|
+
"Take one story. Write a failing test for each acceptance criterion first, then make it pass. Keep the change small, run the repo's checks, and open a pull request that says what you verified.",
|
|
100
|
+
mode: "ask",
|
|
101
|
+
preset: ["git status", "git diff", "git log"],
|
|
102
|
+
category: "engineering",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "reviewer",
|
|
106
|
+
source: "den",
|
|
107
|
+
description:
|
|
108
|
+
"Reviews a pull request adversarially. Never approves or merges.",
|
|
109
|
+
instructions:
|
|
110
|
+
"Check the change against its story's acceptance criteria, then look for what wasn't tested: edge cases, failure paths, security. Verify each finding in the code and report them ranked by impact. Never approve, merge or push.",
|
|
111
|
+
mode: "ask",
|
|
112
|
+
preset: ["git diff", "git log", "gh pr diff", "gh pr view"],
|
|
113
|
+
category: "engineering",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "tester",
|
|
117
|
+
source: "den",
|
|
118
|
+
description: "Runs the test suites and reports failures with repro steps.",
|
|
119
|
+
instructions:
|
|
120
|
+
"Run the workspace's unit, integration and end-to-end suites. For each failure, write the exact repro and the smallest failing command. Don't fix code.",
|
|
121
|
+
mode: "ask",
|
|
122
|
+
preset: ["git status"],
|
|
123
|
+
category: "engineering",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "designer",
|
|
127
|
+
source: "den",
|
|
128
|
+
description: "Designs flows and screens with the design system.",
|
|
129
|
+
instructions:
|
|
130
|
+
"Start from the user's goal and the story's acceptance criteria. Compose from the design system's components, and describe every state (empty, loading, error, success) and narrow screens. Hand off a short spec the developers can build and test.",
|
|
131
|
+
mode: "ask",
|
|
132
|
+
preset: [],
|
|
133
|
+
category: "design",
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const isMapping = (v: unknown): v is Record<string, unknown> =>
|
|
138
|
+
v !== null && typeof v === "object" && !Array.isArray(v);
|
|
139
|
+
const shown = (v: unknown) =>
|
|
140
|
+
(JSON.stringify(v) ?? "undefined").replace(/[^\x20-\x7e]/g, "?").slice(0, 60);
|
|
141
|
+
|
|
142
|
+
function line(where: string, v: unknown, max: number): string {
|
|
143
|
+
if (
|
|
144
|
+
typeof v !== "string" ||
|
|
145
|
+
!v.trim() ||
|
|
146
|
+
[...v].length > max ||
|
|
147
|
+
/[\p{Cc}\p{Cf}]/u.test(v)
|
|
148
|
+
)
|
|
149
|
+
throw new Error(
|
|
150
|
+
`${where}: must be one line of 1-${max} characters, no control characters`,
|
|
151
|
+
);
|
|
152
|
+
return v.trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* `agents.categories` checked: a list of { id, label }. A Den category's
|
|
157
|
+
* id is refused (it can't be renamed). Anything else throws, naming it.
|
|
158
|
+
*/
|
|
159
|
+
export function checkCategoriesConfig(raw: unknown): Category[] {
|
|
160
|
+
if (raw === undefined || raw === null) return [];
|
|
161
|
+
if (!Array.isArray(raw))
|
|
162
|
+
throw new Error("agents.categories: must be a list of { id, label }");
|
|
163
|
+
const seen = new Set(DEN_CATEGORIES.map((c) => c.id));
|
|
164
|
+
return raw.map((x, i) => {
|
|
165
|
+
const where = `agents.categories[${i}]`;
|
|
166
|
+
if (!isMapping(x))
|
|
167
|
+
throw new Error(`${where}: must be a mapping { id, label }`);
|
|
168
|
+
for (const k of Object.keys(x))
|
|
169
|
+
if (k !== "id" && k !== "label")
|
|
170
|
+
throw new Error(`${where}.${k}: unknown key (takes id, label)`);
|
|
171
|
+
if (typeof x.id !== "string" || !REGISTRY_ID_RE.test(x.id))
|
|
172
|
+
throw new Error(
|
|
173
|
+
`${where}.id: ${shown(x.id)} must be lowercase letters, digits and "-" (1-32)`,
|
|
174
|
+
);
|
|
175
|
+
if (seen.has(x.id))
|
|
176
|
+
throw new Error(`${where}.id: "${x.id}" is already a category`);
|
|
177
|
+
seen.add(x.id);
|
|
178
|
+
return {
|
|
179
|
+
id: x.id,
|
|
180
|
+
label: line(`${where}.label`, x.label, 40),
|
|
181
|
+
source: "distribution" as const,
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** What `agents.roles` holds, checked (merged with Den's by roleRegistry). */
|
|
187
|
+
export interface RolesConfig {
|
|
188
|
+
/** New roles. */
|
|
189
|
+
added: RoleTemplate[];
|
|
190
|
+
/** Den roles' preset extensions, by role id. */
|
|
191
|
+
extended: Record<string, string[]>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* `agents.roles` checked. A Den role takes `preset` only (an extension);
|
|
196
|
+
* a new role takes description, instructions, mode, preset and category.
|
|
197
|
+
* `categories` are the ids a new role's category may name.
|
|
198
|
+
*/
|
|
199
|
+
export function checkRolesConfig(
|
|
200
|
+
raw: unknown,
|
|
201
|
+
categories: readonly string[] = DEN_CATEGORIES.map((c) => c.id),
|
|
202
|
+
): RolesConfig {
|
|
203
|
+
const out: RolesConfig = { added: [], extended: {} };
|
|
204
|
+
if (raw === undefined || raw === null) return out;
|
|
205
|
+
if (!isMapping(raw))
|
|
206
|
+
throw new Error(
|
|
207
|
+
"agents.roles: must be a mapping of role id to its template",
|
|
208
|
+
);
|
|
209
|
+
for (const [id, v] of Object.entries(raw)) {
|
|
210
|
+
const where = `agents.roles.${id}`;
|
|
211
|
+
if (!REGISTRY_ID_RE.test(id))
|
|
212
|
+
throw new Error(
|
|
213
|
+
`${where}: a role id must be lowercase letters, digits and "-" (1-32)`,
|
|
214
|
+
);
|
|
215
|
+
if (!isMapping(v)) throw new Error(`${where}: must be a mapping`);
|
|
216
|
+
const den = DEN_ROLES.find((r) => r.id === id);
|
|
217
|
+
if (den) {
|
|
218
|
+
for (const k of Object.keys(v))
|
|
219
|
+
if (k !== "preset")
|
|
220
|
+
throw new Error(
|
|
221
|
+
`${where}.${k}: ${id} is Den's role: a distribution may only extend its preset`,
|
|
222
|
+
);
|
|
223
|
+
out.extended[id] = checkCommands(v.preset, `${where}.preset`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const keys = ["description", "instructions", "mode", "preset", "category"];
|
|
227
|
+
for (const k of Object.keys(v))
|
|
228
|
+
if (!keys.includes(k))
|
|
229
|
+
throw new Error(
|
|
230
|
+
`${where}.${k}: unknown key (takes ${keys.join(", ")})`,
|
|
231
|
+
);
|
|
232
|
+
const mode = v.mode ?? "ask";
|
|
233
|
+
if (!(PERMISSION_INTENTS as readonly unknown[]).includes(mode))
|
|
234
|
+
throw new Error(
|
|
235
|
+
`${where}.mode: ${shown(mode)} must be one of ${PERMISSION_INTENTS.join(", ")}`,
|
|
236
|
+
);
|
|
237
|
+
const category = v.category ?? "engineering";
|
|
238
|
+
if (typeof category !== "string" || !categories.includes(category))
|
|
239
|
+
throw new Error(
|
|
240
|
+
`${where}.category: ${shown(category)} is not a category (${categories.join(", ")})`,
|
|
241
|
+
);
|
|
242
|
+
const instructions = v.instructions ?? "";
|
|
243
|
+
if (typeof instructions !== "string" || instructions.length > 4000)
|
|
244
|
+
throw new Error(
|
|
245
|
+
`${where}.instructions: must be text of up to 4000 characters`,
|
|
246
|
+
);
|
|
247
|
+
out.added.push({
|
|
248
|
+
id,
|
|
249
|
+
source: "distribution",
|
|
250
|
+
description: line(`${where}.description`, v.description, 200),
|
|
251
|
+
instructions: instructions.trim(),
|
|
252
|
+
mode: mode as PermissionIntent,
|
|
253
|
+
preset: checkCommands(v.preset, `${where}.preset`),
|
|
254
|
+
category,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Den's roles with a distribution's extensions, then its new roles. */
|
|
261
|
+
export function roleRegistry(
|
|
262
|
+
cfg: RolesConfig = { added: [], extended: {} },
|
|
263
|
+
): RoleTemplate[] {
|
|
264
|
+
const den = DEN_ROLES.map((r) => {
|
|
265
|
+
const ext = cfg.extended[r.id];
|
|
266
|
+
if (!ext?.length) return { ...r, preset: [...r.preset] };
|
|
267
|
+
return {
|
|
268
|
+
...r,
|
|
269
|
+
preset: [...new Set([...r.preset, ...ext])],
|
|
270
|
+
extendedBy: "distribution" as const,
|
|
271
|
+
extension: [...ext],
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
return [...den, ...cfg.added];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Den's categories, then a distribution's. */
|
|
278
|
+
export function categoryRegistry(extra: readonly Category[] = []): Category[] {
|
|
279
|
+
return [...DEN_CATEGORIES, ...extra];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** A role's command preset ([] for none, or a role that isn't registered). */
|
|
283
|
+
export function rolePreset(
|
|
284
|
+
roles: readonly RoleTemplate[],
|
|
285
|
+
role: string | undefined,
|
|
286
|
+
): string[] {
|
|
287
|
+
return role ? (roles.find((r) => r.id === role)?.preset ?? []) : [];
|
|
288
|
+
}
|
|
@@ -20,6 +20,8 @@ import type { CatalogEntry } from "./catalog.js";
|
|
|
20
20
|
import type { AgentRecord } from "./records.js";
|
|
21
21
|
import type { Persona } from "./personas.js";
|
|
22
22
|
import type { BinSource, RuntimeFind } from "./runtime-search.js";
|
|
23
|
+
import type { PermissionIntent } from "./permissions.js";
|
|
24
|
+
import type { RuntimeConversation } from "./conversations.js";
|
|
23
25
|
|
|
24
26
|
export const RUNTIME_KIND = "runtime";
|
|
25
27
|
|
|
@@ -95,6 +97,16 @@ export interface LaunchInput {
|
|
|
95
97
|
* session rotates (0: never). Den's hooks count them. Default 3.
|
|
96
98
|
*/
|
|
97
99
|
rotateAfter?: number;
|
|
100
|
+
/**
|
|
101
|
+
* The permission intent to launch with (AG-13): the agent's, else its
|
|
102
|
+
* persona's `permission_mode`. Only a runtime with `permissions` takes it.
|
|
103
|
+
*/
|
|
104
|
+
permission?: PermissionIntent;
|
|
105
|
+
/**
|
|
106
|
+
* Plain command prefixes allowed without asking (AG-13.3, 13.4): the
|
|
107
|
+
* role's preset and the extra commands, checked (permissions.ts).
|
|
108
|
+
*/
|
|
109
|
+
allow?: string[];
|
|
98
110
|
/**
|
|
99
111
|
* The new conversation's first prompt (a relaunch from handoff.md).
|
|
100
112
|
* Only a runtime with `firstPrompt: true` is given one.
|
|
@@ -116,6 +128,29 @@ export interface LaunchOutput {
|
|
|
116
128
|
|
|
117
129
|
export type ResumeInput = LaunchInput;
|
|
118
130
|
|
|
131
|
+
/**
|
|
132
|
+
* A model a runtime offers (AG-12.3): its id as the Model list shows it,
|
|
133
|
+
* and the versions it can be pinned to, newest first. "Latest" tracks the
|
|
134
|
+
* newest release; a pinned version is one of `versions`.
|
|
135
|
+
*/
|
|
136
|
+
export interface RuntimeModel {
|
|
137
|
+
id: string;
|
|
138
|
+
label: string;
|
|
139
|
+
versions: string[];
|
|
140
|
+
latest: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* How a runtime expresses Den's permission intents (AG-13.2), as its own
|
|
145
|
+
* launch config: a read-only preview of each mode ("What this allows on
|
|
146
|
+
* <runtime>"), and how one plain command prefix becomes its rules
|
|
147
|
+
* (`{c}` the prefix). The runtime's launch writes exactly these.
|
|
148
|
+
*/
|
|
149
|
+
export interface RuntimePermissions {
|
|
150
|
+
modes: Record<PermissionIntent, string>;
|
|
151
|
+
commandRules: string[];
|
|
152
|
+
}
|
|
153
|
+
|
|
119
154
|
export interface Runtime {
|
|
120
155
|
/** "den-claude", "generic". */
|
|
121
156
|
id: string;
|
|
@@ -142,6 +177,13 @@ export interface Runtime {
|
|
|
142
177
|
* firstPrompt): Relaunch from handoff needs it (den#4).
|
|
143
178
|
*/
|
|
144
179
|
firstPrompt?: boolean;
|
|
180
|
+
/**
|
|
181
|
+
* The models it offers (AG-12.3), from the catalog shipped with each Den
|
|
182
|
+
* release; none: it takes no model choice from Den.
|
|
183
|
+
*/
|
|
184
|
+
models?(): RuntimeModel[];
|
|
185
|
+
/** How it expresses permission intents (AG-13); none: it can't yet. */
|
|
186
|
+
permissions?: RuntimePermissions;
|
|
145
187
|
/** Throws LaunchError when this persona or this machine can't run it. */
|
|
146
188
|
launch(input: LaunchInput): LaunchOutput;
|
|
147
189
|
/** A new run that continues the last one; null when there is nothing to resume. */
|
|
@@ -152,6 +194,14 @@ export interface Runtime {
|
|
|
152
194
|
* just mean it isn't written yet.
|
|
153
195
|
*/
|
|
154
196
|
conversationExists?(token: string, env?: NodeJS.ProcessEnv): boolean;
|
|
197
|
+
/**
|
|
198
|
+
* AG-17.1, bring in a conversation: the runtime's own recent
|
|
199
|
+
* conversations that ran in `cwd` or a folder inside it, newest first,
|
|
200
|
+
* read from its history (only the ends of each file, never a whole
|
|
201
|
+
* transcript). Each id is one `resume` continues. None: this runtime
|
|
202
|
+
* can't bring one in yet. Must never throw.
|
|
203
|
+
*/
|
|
204
|
+
listSessions?(cwd: string, env?: NodeJS.ProcessEnv): RuntimeConversation[];
|
|
155
205
|
/**
|
|
156
206
|
* What the runtime prints when it exits at once (0.4.8): `noConversation`
|
|
157
207
|
* when a resume finds nothing to continue, `idInUse` when a new
|
package/src/shared/den-config.ts
CHANGED
|
@@ -21,6 +21,13 @@ import { ConfigError, isConfigError } from "./config-error.js";
|
|
|
21
21
|
import { validateCatalog, type CatalogEntry } from "./agents/catalog.js";
|
|
22
22
|
import type { Guardrails } from "./agents/guardrails.js";
|
|
23
23
|
import { DEFAULT_ROTATE, type RotateSettings } from "./agents/rotation.js";
|
|
24
|
+
import {
|
|
25
|
+
checkCategoriesConfig,
|
|
26
|
+
checkRolesConfig,
|
|
27
|
+
DEN_CATEGORIES,
|
|
28
|
+
type Category,
|
|
29
|
+
type RolesConfig,
|
|
30
|
+
} from "./agents/roles.js";
|
|
24
31
|
import type { TestAccountsConfig } from "./auth/test-accounts.js";
|
|
25
32
|
|
|
26
33
|
import { product } from "./product.js";
|
|
@@ -811,6 +818,8 @@ export const MCP_ACTIONS = [
|
|
|
811
818
|
"run_tests",
|
|
812
819
|
// AG-14.4: resume another agent of its workspace (or a granted one).
|
|
813
820
|
"agent_resume",
|
|
821
|
+
// AG-17.1: bring in a conversation started outside Den, as an agent.
|
|
822
|
+
"agent_import",
|
|
814
823
|
] as const;
|
|
815
824
|
export type McpAction = (typeof MCP_ACTIONS)[number];
|
|
816
825
|
|
|
@@ -918,6 +927,9 @@ export const AGENTS_RESERVED_KEYS = [
|
|
|
918
927
|
"runtime_catalog",
|
|
919
928
|
"default_pack",
|
|
920
929
|
"rotate",
|
|
930
|
+
// AG-10.5, AG-11.1: the distribution's persona categories and roles.
|
|
931
|
+
"categories",
|
|
932
|
+
"roles",
|
|
921
933
|
];
|
|
922
934
|
|
|
923
935
|
/** One agent's own block, `agents.<id>`. */
|
|
@@ -941,6 +953,10 @@ export interface ResolvedAgentsConfig {
|
|
|
941
953
|
runtimeCatalog: CatalogEntry[];
|
|
942
954
|
/** agents.rotate with its defaults (den#4); unset: DEFAULT_ROTATE. */
|
|
943
955
|
rotate?: RotateSettings;
|
|
956
|
+
/** agents.categories (AG-10.5): the distribution's, after Den's. */
|
|
957
|
+
categories?: Category[];
|
|
958
|
+
/** agents.roles (AG-11.1): new roles and Den roles' preset extensions. */
|
|
959
|
+
roles?: RolesConfig;
|
|
944
960
|
}
|
|
945
961
|
|
|
946
962
|
/** `agents.rotate` checked; any other key, or a bad value, throws naming it. */
|
|
@@ -1034,6 +1050,11 @@ export function readAgentsConfig(
|
|
|
1034
1050
|
),
|
|
1035
1051
|
rotate: rotateBlock(raw.rotate),
|
|
1036
1052
|
};
|
|
1053
|
+
out.categories = checkCategoriesConfig(raw.categories);
|
|
1054
|
+
out.roles = checkRolesConfig(raw.roles, [
|
|
1055
|
+
...DEN_CATEGORIES.map((c) => c.id),
|
|
1056
|
+
...out.categories.map((c) => c.id),
|
|
1057
|
+
]);
|
|
1037
1058
|
const max = raw.max_concurrent;
|
|
1038
1059
|
if (max !== undefined) {
|
|
1039
1060
|
if (typeof max !== "number" || !Number.isInteger(max) || max < 1)
|
|
@@ -1066,7 +1087,7 @@ export function readAgentsConfig(
|
|
|
1066
1087
|
const where = `agents.${id}`;
|
|
1067
1088
|
if (!AGENT_KEY_RE.test(id) || !isMapping(block))
|
|
1068
1089
|
throw new Error(
|
|
1069
|
-
`${where}: unknown key (agents: takes max_concurrent, persona_packs, runtime_catalog, default_pack, rotate, ` +
|
|
1090
|
+
`${where}: unknown key (agents: takes max_concurrent, persona_packs, runtime_catalog, default_pack, rotate, categories, roles, ` +
|
|
1070
1091
|
`or an agent id mapping to { grants: [...] })`,
|
|
1071
1092
|
);
|
|
1072
1093
|
for (const k of Object.keys(block))
|
|
@@ -2794,18 +2794,13 @@ export function runServiceCommand(
|
|
|
2794
2794
|
// later down signalled the dead group while the real listener ran on).
|
|
2795
2795
|
// A failed start keeps no record at all.
|
|
2796
2796
|
const serviceExited = (exitCode: number): void => {
|
|
2797
|
-
const rec = recorded;
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
if (now?.pgid === rec.pgid && exitCode !== 0) {
|
|
2805
|
-
if (!(now.containers?.length ?? 0)) unregisterDaemon(resolved.address);
|
|
2806
|
-
else clearDaemonProcessGroup(resolved.address);
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2797
|
+
const rec: RecordedProcessGroup | null = recorded;
|
|
2798
|
+
const state = rec ? (processGroupStateSync(rec)?.state ?? null) : null;
|
|
2799
|
+
const now = findDaemonByAddress(resolved.address);
|
|
2800
|
+
const action = failedStartCleanup(exitCode, rec, state, now);
|
|
2801
|
+
if (action === "unregister") unregisterDaemon(resolved.address);
|
|
2802
|
+
else if (action === "clear-group")
|
|
2803
|
+
clearDaemonProcessGroup(resolved.address);
|
|
2809
2804
|
onServiceExit?.(exitCode);
|
|
2810
2805
|
};
|
|
2811
2806
|
const proc = recordGroup
|
|
@@ -2835,6 +2830,30 @@ export function runServiceCommand(
|
|
|
2835
2830
|
return { proc, detachable, interactive: false, logPath };
|
|
2836
2831
|
}
|
|
2837
2832
|
|
|
2833
|
+
/**
|
|
2834
|
+
* What a detached up does with its record when the service process exits.
|
|
2835
|
+
* A non-zero exit is a failed start: nothing of it runs, so no record (its
|
|
2836
|
+
* containers, if any, still run: then only the group goes). A zero exit
|
|
2837
|
+
* handed off (`make up`): its group stays, and down says it already exited.
|
|
2838
|
+
* The record goes only if it is this run's: its group is the one this run
|
|
2839
|
+
* recorded and that group is gone, or this run could not record a group
|
|
2840
|
+
* (the command exited, a zombie, before `ps` saw it) and the record names
|
|
2841
|
+
* none.
|
|
2842
|
+
*/
|
|
2843
|
+
export function failedStartCleanup(
|
|
2844
|
+
exitCode: number,
|
|
2845
|
+
rec: RecordedProcessGroup | null,
|
|
2846
|
+
groupState: string | null,
|
|
2847
|
+
now: { pgid?: number; containers?: unknown[] } | undefined,
|
|
2848
|
+
): "unregister" | "clear-group" | "keep" {
|
|
2849
|
+
if (exitCode === 0 || !now) return "keep";
|
|
2850
|
+
const ours = rec
|
|
2851
|
+
? groupState === "gone" && now.pgid === rec.pgid
|
|
2852
|
+
: now.pgid === undefined;
|
|
2853
|
+
if (!ours) return "keep";
|
|
2854
|
+
return (now.containers?.length ?? 0) ? "clear-group" : "unregister";
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2838
2857
|
// ── Health checks ─────────────────────────────────────────────────────────────
|
|
2839
2858
|
// (implementation lives in health.ts — re-exported above)
|
|
2840
2859
|
|
|
@@ -344,6 +344,17 @@ export function resolveWorkspaceDir(
|
|
|
344
344
|
return reposRoot;
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
/**
|
|
348
|
+
* The folders of a workspace's by-path repos (`repo add <path>`), wherever
|
|
349
|
+
* they are, outside its own folder too (AG-17.1). Managed clones and a
|
|
350
|
+
* feature workspace's clones live under Den's or the workspace's folder.
|
|
351
|
+
*/
|
|
352
|
+
export function repoFolders(cfg: WorkspaceConfig | undefined): string[] {
|
|
353
|
+
return Object.values(cfg?.services ?? {})
|
|
354
|
+
.map((e) => (e as { path?: string }).path)
|
|
355
|
+
.filter((p): p is string => typeof p === "string" && path.isAbsolute(p));
|
|
356
|
+
}
|
|
357
|
+
|
|
347
358
|
export function getWorkspaceByName(name: string): {
|
|
348
359
|
name: string;
|
|
349
360
|
config: WorkspaceConfig;
|