@monster-orange/den 0.4.22 → 0.4.23
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 +47 -0
- package/docs/distributions.md +91 -30
- package/package.json +1 -1
- package/src/shared/agents/guardrails/rotation-hook.ts +5 -0
- package/src/shared/agents/rotate-settings.ts +309 -0
- package/src/shared/agents/rotation.ts +110 -5
- package/src/shared/den-config.ts +27 -12
- package/src/ui/cli/agent-ops.ts +449 -15
- package/src/ui/cli/agents-command.ts +169 -3
- package/src/ui/cli/server-agent-ops.ts +2 -2
- package/src/ui/cli/web-command.ts +32 -5
- package/src/ui/cli/web-service.ts +160 -0
- package/src/ui/server/agent-routes.ts +149 -5
- package/src/ui/server/index.ts +13 -5
- package/src/ui/server/public/agents-page.js +214 -22
- package/src/ui/server/public/agents.html +32 -0
- package/src/ui/server/public/i18n/es.js +44 -2
- package/src/ui/server/public/i18n/ja.js +40 -2
- package/src/ui/server/public/settings-registry.js +204 -0
- package/src/ui/server/public/settings.html +43 -4
- package/src/ui/server/public/shell.js +42 -2
- package/src/ui/server/route-classes.ts +21 -0
- package/src/ui/server/session-routes.ts +23 -5
|
@@ -56,6 +56,13 @@ export interface RotationState {
|
|
|
56
56
|
/** Set when a trigger fired. */
|
|
57
57
|
due?: RotateReason;
|
|
58
58
|
dueAt?: string;
|
|
59
|
+
/** D28: how many times Den asked for the handoff, and the last time. */
|
|
60
|
+
asked?: number;
|
|
61
|
+
askedAt?: string;
|
|
62
|
+
/** D28: auto gave up asking (2 asks, still no handoff): ask mode. */
|
|
63
|
+
fellBack?: boolean;
|
|
64
|
+
/** D28 "Relaunch now" before a handoff: relaunch once it passes, whatever on_due says. */
|
|
65
|
+
relaunchWanted?: boolean;
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
/** `agents.rotate` with its defaults applied. */
|
|
@@ -80,6 +87,8 @@ export interface RotationEntry {
|
|
|
80
87
|
compactions: number;
|
|
81
88
|
/** The conversation that ended (its resume token). */
|
|
82
89
|
conversation?: string;
|
|
90
|
+
/** D28: Den relaunched it by itself (on_due auto). */
|
|
91
|
+
auto?: boolean;
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
/** The compactions after which `persona` rotates; 0: it never does. */
|
|
@@ -119,6 +128,10 @@ export function readRotation(filesDir: string): RotationState | null {
|
|
|
119
128
|
if (typeof o.pendingTrigger === "string") s.pendingTrigger = o.pendingTrigger;
|
|
120
129
|
if (o.due === "compactions" || o.due === "pr-merged") s.due = o.due;
|
|
121
130
|
if (typeof o.dueAt === "string") s.dueAt = o.dueAt;
|
|
131
|
+
if (count(o.asked)) s.asked = count(o.asked);
|
|
132
|
+
if (typeof o.askedAt === "string") s.askedAt = o.askedAt;
|
|
133
|
+
if (o.fellBack === true) s.fellBack = true;
|
|
134
|
+
if (o.relaunchWanted === true) s.relaunchWanted = true;
|
|
122
135
|
return s;
|
|
123
136
|
}
|
|
124
137
|
|
|
@@ -235,6 +248,51 @@ export function markDue(
|
|
|
235
248
|
return true;
|
|
236
249
|
}
|
|
237
250
|
|
|
251
|
+
/**
|
|
252
|
+
* D28 "Keep this conversation": the count starts again from 0 for the
|
|
253
|
+
* conversation it has, and nothing is due. Returns false with no state.
|
|
254
|
+
*/
|
|
255
|
+
export function keepConversation(filesDir: string): boolean {
|
|
256
|
+
const s = readRotation(filesDir);
|
|
257
|
+
if (!s) return false;
|
|
258
|
+
writeRotation(filesDir, {
|
|
259
|
+
...(s.conversation ? { conversation: s.conversation } : {}),
|
|
260
|
+
compactions: 0,
|
|
261
|
+
after: s.after,
|
|
262
|
+
});
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** D28: one more ask for the handoff, noted; `relaunch`: Relaunch now asked. */
|
|
267
|
+
export function noteAsked(
|
|
268
|
+
filesDir: string,
|
|
269
|
+
now: Date = new Date(),
|
|
270
|
+
relaunch = false,
|
|
271
|
+
): RotationState | null {
|
|
272
|
+
const s = readRotation(filesDir);
|
|
273
|
+
if (!s) return null;
|
|
274
|
+
const next: RotationState = {
|
|
275
|
+
...s,
|
|
276
|
+
asked: (s.asked ?? 0) + 1,
|
|
277
|
+
askedAt: now.toISOString(),
|
|
278
|
+
...(relaunch ? { relaunchWanted: true } : {}),
|
|
279
|
+
};
|
|
280
|
+
writeRotation(filesDir, next);
|
|
281
|
+
return next;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** D28: auto gave up asking; the callout's buttons take over. */
|
|
285
|
+
export function noteFellBack(filesDir: string): void {
|
|
286
|
+
const s = readRotation(filesDir);
|
|
287
|
+
if (s && !s.fellBack) writeRotation(filesDir, { ...s, fellBack: true });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** D28: the limit the hook counts against, when the settings changed. */
|
|
291
|
+
export function syncAfter(filesDir: string, after: number): void {
|
|
292
|
+
const s = readRotation(filesDir);
|
|
293
|
+
if (s && s.after !== after) writeRotation(filesDir, { ...s, after });
|
|
294
|
+
}
|
|
295
|
+
|
|
238
296
|
/** Clear a rotation due for `reason` (a merge mark gh no longer confirms). */
|
|
239
297
|
export function clearDue(filesDir: string, reason: RotateReason): boolean {
|
|
240
298
|
const s = readRotation(filesDir);
|
|
@@ -274,8 +332,31 @@ export function readHandoff(filesDir: string): {
|
|
|
274
332
|
return { status: r.ok ? "ok" : "invalid", text, problems: r.problems };
|
|
275
333
|
}
|
|
276
334
|
|
|
335
|
+
/**
|
|
336
|
+
* D28: an agent's effective rotation settings (rotate-settings.ts), when
|
|
337
|
+
* the caller resolved them; without, the state's own limit and den#4's
|
|
338
|
+
* rules apply.
|
|
339
|
+
*/
|
|
340
|
+
export interface RotationPolicy {
|
|
341
|
+
/** 0: never rotates. */
|
|
342
|
+
afterCompactions: number;
|
|
343
|
+
onDue: "auto" | "ask" | "off";
|
|
344
|
+
onPrMerged: boolean;
|
|
345
|
+
/** Where each value comes from, in words ("persona planner"). */
|
|
346
|
+
from?: { after_compactions: string; on_due: string; on_pr_merged: string };
|
|
347
|
+
}
|
|
348
|
+
|
|
277
349
|
/** An agent's rotation, as the Agents page and the CLI show it. */
|
|
278
350
|
export interface RotationInfo {
|
|
351
|
+
/** D28: what happens when it is due, and where the settings come from. */
|
|
352
|
+
onDue?: RotationPolicy["onDue"];
|
|
353
|
+
from?: RotationPolicy["from"];
|
|
354
|
+
/** D28: Den asked it for its handoff this many times (auto). */
|
|
355
|
+
asked?: number;
|
|
356
|
+
/** D28: auto fell back to ask (the handoff still failed after 2 asks). */
|
|
357
|
+
fellBack?: boolean;
|
|
358
|
+
/** D28: Relaunch now was asked before the handoff: Den relaunches once it passes. */
|
|
359
|
+
relaunchWanted?: boolean;
|
|
279
360
|
/** Why it rotates, when a trigger fired. */
|
|
280
361
|
due?: RotateReason;
|
|
281
362
|
compactions: number;
|
|
@@ -295,20 +376,44 @@ export interface RotationInfo {
|
|
|
295
376
|
export function rotationInfo(
|
|
296
377
|
stateDir: string,
|
|
297
378
|
rec: AgentRecord,
|
|
379
|
+
policy?: RotationPolicy,
|
|
298
380
|
): RotationInfo | null {
|
|
299
381
|
const files = agentFilesDir(stateDir, rec.id);
|
|
300
|
-
const
|
|
301
|
-
if (!
|
|
302
|
-
if (
|
|
382
|
+
const read = readRotation(files);
|
|
383
|
+
if (!read) return null;
|
|
384
|
+
if (
|
|
385
|
+
read.conversation &&
|
|
386
|
+
rec.resumeToken &&
|
|
387
|
+
read.conversation !== rec.resumeToken
|
|
388
|
+
)
|
|
303
389
|
return null;
|
|
390
|
+
// D28: the limit in force now, not the one the launch counted against.
|
|
391
|
+
const s: RotationState = policy
|
|
392
|
+
? { ...read, after: policy.afterCompactions }
|
|
393
|
+
: read;
|
|
394
|
+
if (
|
|
395
|
+
policy &&
|
|
396
|
+
s.after > 0 &&
|
|
397
|
+
s.compactions < s.after &&
|
|
398
|
+
s.due === "compactions"
|
|
399
|
+
) {
|
|
400
|
+
delete s.due;
|
|
401
|
+
}
|
|
402
|
+
if (policy && !policy.onPrMerged && s.due === "pr-merged") delete s.due;
|
|
403
|
+
const merged = policy ? policy.onPrMerged : true;
|
|
304
404
|
const due =
|
|
305
405
|
dueFor(s) ??
|
|
306
|
-
(s.after > 0 && rec.outcome?.prMerged ? "pr-merged" : undefined);
|
|
406
|
+
(s.after > 0 && merged && rec.outcome?.prMerged ? "pr-merged" : undefined);
|
|
307
407
|
const h = due
|
|
308
408
|
? readHandoff(files)
|
|
309
409
|
: { status: "missing" as const, problems: [] };
|
|
310
410
|
return {
|
|
311
411
|
...(due ? { due } : {}),
|
|
412
|
+
...(policy ? { onDue: policy.onDue } : {}),
|
|
413
|
+
...(policy?.from ? { from: policy.from } : {}),
|
|
414
|
+
...(read.asked ? { asked: read.asked } : {}),
|
|
415
|
+
...(read.fellBack ? { fellBack: true } : {}),
|
|
416
|
+
...(read.relaunchWanted ? { relaunchWanted: true } : {}),
|
|
312
417
|
compactions: s.compactions,
|
|
313
418
|
after: s.after,
|
|
314
419
|
handoff: h.status,
|
|
@@ -346,7 +451,7 @@ export function rotationInstruction(
|
|
|
346
451
|
const why =
|
|
347
452
|
reason === "pr-merged"
|
|
348
453
|
? "its pull request merged"
|
|
349
|
-
: `it has compacted ${compactions} times`;
|
|
454
|
+
: `it has compacted ${compactions === 1 ? "once" : `${compactions} times`}`;
|
|
350
455
|
return [
|
|
351
456
|
`Den: this session is done, because ${why} (session rotation).`,
|
|
352
457
|
`Write your handoff now, to ${handoffFile(filesDir)}, with these headings, each filled in:`,
|
package/src/shared/den-config.ts
CHANGED
|
@@ -21,6 +21,7 @@ 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 { checkRotateValue, type OrgRotate } from "./agents/rotate-settings.js";
|
|
24
25
|
import {
|
|
25
26
|
checkCategoriesConfig,
|
|
26
27
|
checkRolesConfig,
|
|
@@ -965,6 +966,8 @@ export interface ResolvedAgentsConfig {
|
|
|
965
966
|
runtimeCatalog: CatalogEntry[];
|
|
966
967
|
/** agents.rotate with its defaults (den#4); unset: DEFAULT_ROTATE. */
|
|
967
968
|
rotate?: RotateSettings;
|
|
969
|
+
/** D28: agents.rotate as written, the org scope. */
|
|
970
|
+
rotateOrg?: OrgRotate;
|
|
968
971
|
/** agents.categories (AG-10.5): the distribution's, after Den's. */
|
|
969
972
|
categories?: Category[];
|
|
970
973
|
/** agents.roles (AG-11.1): new roles and Den roles' preset extensions. */
|
|
@@ -972,32 +975,41 @@ export interface ResolvedAgentsConfig {
|
|
|
972
975
|
}
|
|
973
976
|
|
|
974
977
|
/** `agents.rotate` checked; any other key, or a bad value, throws naming it. */
|
|
975
|
-
|
|
978
|
+
/**
|
|
979
|
+
* `agents.rotate` checked, both ways: den#4's settings (afterCompactions 0
|
|
980
|
+
* for never) and, for D28's scopes, the org's values as written.
|
|
981
|
+
*/
|
|
982
|
+
export function rotateOrgBlock(v: unknown): {
|
|
983
|
+
settings: RotateSettings;
|
|
984
|
+
org: OrgRotate;
|
|
985
|
+
} {
|
|
976
986
|
const out: RotateSettings = { ...DEFAULT_ROTATE };
|
|
977
|
-
|
|
987
|
+
const org: OrgRotate = {};
|
|
988
|
+
if (v === undefined || v === null) return { settings: out, org };
|
|
978
989
|
if (!isMapping(v))
|
|
979
990
|
throw new Error(
|
|
980
|
-
"agents.rotate: must be a mapping ({ after_compactions: <n
|
|
991
|
+
"agents.rotate: must be a mapping ({ after_compactions: <n>|never, on_due: auto|ask|off, on_pr_merged: true|false })",
|
|
981
992
|
);
|
|
982
993
|
for (const [k, x] of Object.entries(v)) {
|
|
983
|
-
if (k === "after_compactions") {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
994
|
+
if (k === "after_compactions" || k === "on_due" || k === "on_pr_merged") {
|
|
995
|
+
const bad = checkRotateValue(k, x);
|
|
996
|
+
if (bad) throw new Error(`agents.rotate.${bad}`);
|
|
997
|
+
(org as Record<string, unknown>)[k] = x;
|
|
998
|
+
if (k === "after_compactions")
|
|
999
|
+
out.afterCompactions = x === "never" ? 0 : (x as number);
|
|
989
1000
|
} else if (k === "standing") {
|
|
990
1001
|
if (typeof x !== "boolean")
|
|
991
1002
|
throw new Error(
|
|
992
1003
|
`agents.rotate.standing: must be true or false (got ${JSON.stringify(x)})`,
|
|
993
1004
|
);
|
|
994
1005
|
out.standing = x;
|
|
1006
|
+
org.standing = x;
|
|
995
1007
|
} else
|
|
996
1008
|
throw new Error(
|
|
997
|
-
`agents.rotate.${k}: unknown key (takes after_compactions,
|
|
1009
|
+
`agents.rotate.${k}: unknown key (takes after_compactions, on_due, on_pr_merged)`,
|
|
998
1010
|
);
|
|
999
1011
|
}
|
|
1000
|
-
return out;
|
|
1012
|
+
return { settings: out, org };
|
|
1001
1013
|
}
|
|
1002
1014
|
|
|
1003
1015
|
/** An agent id or workspace name: what WORKSPACE_NAME_RE accepts. */
|
|
@@ -1060,7 +1072,10 @@ export function readAgentsConfig(
|
|
|
1060
1072
|
raw.runtime_catalog,
|
|
1061
1073
|
"agents.runtime_catalog",
|
|
1062
1074
|
),
|
|
1063
|
-
|
|
1075
|
+
...(() => {
|
|
1076
|
+
const r = rotateOrgBlock(raw.rotate);
|
|
1077
|
+
return { rotate: r.settings, rotateOrg: r.org };
|
|
1078
|
+
})(),
|
|
1064
1079
|
};
|
|
1065
1080
|
out.categories = checkCategoriesConfig(raw.categories);
|
|
1066
1081
|
out.roles = checkRolesConfig(raw.roles, [
|