@a-t-h-i/bot-lobby 0.6.53 → 0.6.55
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 +1 -1
- package/package.json +1 -1
- package/prompts/expert.md +17 -0
- package/prompts/goal-check.md +44 -0
- package/prompts/master.md +70 -32
- package/prompts/worker.md +25 -11
- package/src/desk/board-client.ts +65 -0
- package/src/desk/board.ts +200 -0
- package/src/desk/client-extension.ts +31 -1
- package/src/desk/ipc.ts +16 -4
- package/src/desk/session.ts +129 -1
- package/src/execution/agent-runner.ts +27 -132
- package/src/execution/delegation.ts +112 -38
- package/src/execution/live-run.ts +3 -14
- package/src/execution/pi-runner.ts +30 -163
- package/src/execution/worker-trees.ts +396 -0
- package/src/index.ts +6 -0
- package/src/lobby/feed.ts +1 -1
- package/src/lobby/host.ts +2 -0
- package/src/lobby/planner.ts +2 -2
- package/src/lobby/pr-review.ts +3 -3
- package/src/lobby/quickfix.ts +4 -3
- package/src/lobby/recovery.ts +2 -10
- package/src/lobby/runtime.ts +1 -1
- package/src/lobby/service.ts +3 -1
- package/src/master/goal-check.ts +114 -0
- package/src/master/master.ts +60 -24
- package/src/master/research.ts +3 -6
- package/src/pi/commands.ts +5 -45
- package/src/pi/delegation.ts +23 -4
- package/src/pi/events.ts +6 -19
- package/src/pi/grants.ts +89 -0
- package/src/pi/model-settings.ts +2 -4
- package/src/pi/model-support.ts +12 -5
- package/src/pi/route.ts +1 -1
- package/src/pi/run-summary.ts +3 -7
- package/src/pi/start-flags.ts +3 -3
- package/src/pi/start-task.ts +2 -8
- package/src/pi/tools.ts +10 -12
- package/src/roles/worker.ts +0 -18
- package/src/schemas/configuration.ts +81 -39
- package/src/schemas/findings.ts +0 -12
- package/src/schemas/task.ts +12 -1
- package/src/state/metrics.ts +1 -1
- package/src/state/work-clock.ts +140 -0
- package/src/state/work-time.ts +4 -7
- package/src/webui/api/index.ts +2 -1
- package/src/webui/api/settings.ts +6 -18
- package/src/webui/dev/fake-service.ts +14 -0
- package/src/webui/protocol.ts +23 -0
- package/src/workflow/workflow.ts +204 -344
- package/webui/dist/assets/{TerminalView-CbRc77C8.js → TerminalView-CIfjkb5H.js} +1 -1
- package/webui/dist/assets/index-BkXhtB6A.css +2 -0
- package/webui/dist/assets/index-DIM2-FUQ.js +114 -0
- package/webui/dist/build.json +10 -6
- package/webui/dist/index.html +2 -2
- package/src/state/budget.ts +0 -301
- package/src/state/step-limits.ts +0 -41
- package/webui/dist/assets/index-Cca5uEVs.js +0 -115
- package/webui/dist/assets/index-DsVdXmh5.css +0 -2
|
@@ -29,17 +29,18 @@ export interface AgentModelConfig {
|
|
|
29
29
|
instructions?: string;
|
|
30
30
|
/** Exact native MCP tool names this profile may call; empty grants none. */
|
|
31
31
|
mcpTools?: string[];
|
|
32
|
-
/**
|
|
33
|
-
|
|
32
|
+
/** Names of the skills this agent may load; absent gives it every skill pi finds, empty gives it none. */
|
|
33
|
+
skills?: string[];
|
|
34
34
|
/** Compact this agent's context on its own as it fills (on unless false); the oracle's also updates the knowledge base. */
|
|
35
35
|
autoCompact?: boolean;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** Scouts pick a model
|
|
38
|
+
/** Scouts pick a model only; their thinking is fixed at `SCOUT_THINKING`. */
|
|
39
39
|
export interface ScoutConfig {
|
|
40
40
|
mcpTools?: string[];
|
|
41
|
+
/** See AgentModelConfig.skills. */
|
|
42
|
+
skills?: string[];
|
|
41
43
|
model: ModelRef;
|
|
42
|
-
timeoutMs: number;
|
|
43
44
|
/** Tool calls a scout may make: it is told to wrap up at 60% and refused more past this, so it reports with what it has; 0 = no limit. */
|
|
44
45
|
maxToolCalls: number;
|
|
45
46
|
/** Compact a scout's context on its own as it fills (on unless false). */
|
|
@@ -74,21 +75,16 @@ export interface WorkflowConfig {
|
|
|
74
75
|
requireApprovalForFeatures: boolean;
|
|
75
76
|
requireApprovalForDependencies: boolean;
|
|
76
77
|
requireApprovalForArchitectureChanges: boolean;
|
|
77
|
-
|
|
78
|
-
/** Bounded retries for transient agent failures (crash/stall), §59. A spent deadline never retries. */
|
|
78
|
+
/** Bounded retries for transient agent failures (crash/stall), §59. A cancel never retries. */
|
|
79
79
|
maxAgentRetries: number;
|
|
80
80
|
/** Kill a subagent after this long without any output; 0 disables. */
|
|
81
81
|
stallTimeoutMs: number;
|
|
82
82
|
/** Silence allowed while a single tool call runs (tests, builds); 0 disables. */
|
|
83
83
|
toolStallTimeoutMs: number;
|
|
84
|
-
/** Fraction of the time limit at which an agent is asked to wrap up and report; 0 disables. */
|
|
85
|
-
wrapUpAt: number;
|
|
86
84
|
/** Workers that may run at once when the Master delegates several domains together. */
|
|
87
85
|
maxParallelWorkers: number;
|
|
88
86
|
/** The oracle starts each task with a clean context: its model is sent only the conversation since the task started, or since the last one ended. */
|
|
89
87
|
freshContext: boolean;
|
|
90
|
-
/** Minutes of work time a new task gets unless it is started with its own (`--budget`); 0 = no budget. */
|
|
91
|
-
taskBudgetMinutes: number;
|
|
92
88
|
/** Small, clear, low-risk requests take the fast track (no scouts, proposal or plan; QA only when tests are needed); false puts every task on the full workflow. */
|
|
93
89
|
fastTrack: boolean;
|
|
94
90
|
/** A delegation that names nothing concrete, or is long without done criteria, is sent back to the oracle once for a fuller brief (sending it again unchanged goes through). */
|
|
@@ -101,6 +97,24 @@ export interface WorkflowConfig {
|
|
|
101
97
|
repoMap: boolean;
|
|
102
98
|
/** A scout asked the same thing again for the same task, on a tree that has not changed, reuses its findings instead of running. */
|
|
103
99
|
reuseScouts: boolean;
|
|
100
|
+
/** How a parallel batch shares the repository: one checkout with the file desk (`desk`), or a worktree per worker whose changes are merged back when the batch ends (`worktrees`). */
|
|
101
|
+
parallelMode: ParallelMode;
|
|
102
|
+
/** The QA gate reviews with a clean context: the request, plan, board contracts and diff, not the workers' own accounts of their work. */
|
|
103
|
+
cleanReview: boolean;
|
|
104
|
+
/** Before a task completes, one read-only agent checks the change against the user's original request (not the plan) and names any gap. */
|
|
105
|
+
goalCheck: boolean;
|
|
106
|
+
/** Workers may ask a stronger model for advice on a hard step (`ask_expert`), a couple of times per step. */
|
|
107
|
+
consultExpert: boolean;
|
|
108
|
+
/** The model `ask_expert` consults; empty uses the oracle's. */
|
|
109
|
+
expertModel: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** How workers in one parallel batch share the repository. */
|
|
113
|
+
export const PARALLEL_MODES = ["desk", "worktrees"] as const;
|
|
114
|
+
export type ParallelMode = (typeof PARALLEL_MODES)[number];
|
|
115
|
+
|
|
116
|
+
export function isParallelMode(value: unknown): value is ParallelMode {
|
|
117
|
+
return typeof value === "string" && (PARALLEL_MODES as readonly string[]).includes(value);
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
/**
|
|
@@ -249,7 +263,7 @@ export interface BotLobbyConfig {
|
|
|
249
263
|
researcher: AgentModelConfig;
|
|
250
264
|
/** Direct quick fixes from the lobby: no scouting, planning or review. */
|
|
251
265
|
quickFix: AgentModelConfig;
|
|
252
|
-
/** The task planner that grills the user until a plan is clear
|
|
266
|
+
/** The task planner that grills the user until a plan is clear. */
|
|
253
267
|
planner: AgentModelConfig;
|
|
254
268
|
workflow: WorkflowConfig;
|
|
255
269
|
/** The engine lints the files a task's agents touched, after each step, before QA and at completion. */
|
|
@@ -262,34 +276,36 @@ export interface BotLobbyConfig {
|
|
|
262
276
|
export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
263
277
|
master: { model: INHERIT_MODEL, thinking: "high", instructions: "" },
|
|
264
278
|
agents: {
|
|
265
|
-
designer: { model: INHERIT_MODEL, thinking: DEFAULT_THINKING, instructions: ""
|
|
266
|
-
backend: { model: INHERIT_MODEL, thinking: DEFAULT_THINKING, instructions: ""
|
|
267
|
-
qa: { model: INHERIT_MODEL, thinking: DEFAULT_THINKING, instructions: ""
|
|
279
|
+
designer: { model: INHERIT_MODEL, thinking: DEFAULT_THINKING, instructions: "" },
|
|
280
|
+
backend: { model: INHERIT_MODEL, thinking: DEFAULT_THINKING, instructions: "" },
|
|
281
|
+
qa: { model: INHERIT_MODEL, thinking: DEFAULT_THINKING, instructions: "" },
|
|
268
282
|
},
|
|
269
|
-
scout: { model: INHERIT_MODEL,
|
|
270
|
-
researcher: { model: INHERIT_MODEL, thinking: "low", instructions: ""
|
|
271
|
-
quickFix: { model: INHERIT_MODEL, thinking: "low", instructions: ""
|
|
272
|
-
planner: { model: INHERIT_MODEL, thinking: "high", instructions: ""
|
|
283
|
+
scout: { model: INHERIT_MODEL, maxToolCalls: 30 },
|
|
284
|
+
researcher: { model: INHERIT_MODEL, thinking: "low", instructions: "" },
|
|
285
|
+
quickFix: { model: INHERIT_MODEL, thinking: "low", instructions: "" },
|
|
286
|
+
planner: { model: INHERIT_MODEL, thinking: "high", instructions: "" },
|
|
273
287
|
workflow: {
|
|
274
288
|
maxReviewIterations: 2,
|
|
275
289
|
maxParallelScouts: 3,
|
|
276
290
|
requireApprovalForFeatures: true,
|
|
277
291
|
requireApprovalForDependencies: true,
|
|
278
292
|
requireApprovalForArchitectureChanges: true,
|
|
279
|
-
agentTimeoutMs: 15 * 60 * 1000,
|
|
280
293
|
maxAgentRetries: 1,
|
|
281
294
|
stallTimeoutMs: 5 * 60 * 1000,
|
|
282
295
|
toolStallTimeoutMs: 10 * 60 * 1000,
|
|
283
|
-
wrapUpAt: 0.75,
|
|
284
296
|
maxParallelWorkers: 3,
|
|
285
297
|
freshContext: true,
|
|
286
|
-
taskBudgetMinutes: 0,
|
|
287
298
|
fastTrack: true,
|
|
288
299
|
briefCheck: true,
|
|
289
300
|
routeQuickFixes: true,
|
|
290
301
|
gitIsolation: "off",
|
|
291
302
|
repoMap: true,
|
|
292
303
|
reuseScouts: true,
|
|
304
|
+
parallelMode: "desk",
|
|
305
|
+
cleanReview: true,
|
|
306
|
+
goalCheck: true,
|
|
307
|
+
consultExpert: true,
|
|
308
|
+
expertModel: "",
|
|
293
309
|
},
|
|
294
310
|
lint: { mode: "advise", command: "", extensions: [], timeoutMs: 2 * 60 * 1000 },
|
|
295
311
|
knowledge: {
|
|
@@ -345,6 +361,17 @@ function positive(value: unknown): number | undefined {
|
|
|
345
361
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
346
362
|
}
|
|
347
363
|
|
|
364
|
+
/** Skill names as pi writes them (lowercase letters, digits and hyphens); absent when the value is not a list, so the agent keeps every skill. */
|
|
365
|
+
export function normalizeSkills(value: unknown): string[] | undefined {
|
|
366
|
+
return Array.isArray(value) ? [...new Set(value.filter((name): name is string => typeof name === "string" && /^[a-z0-9][a-z0-9-]{0,63}$/.test(name)))] : undefined;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** `{ skills }` when an entry names its skills, else nothing. */
|
|
370
|
+
function skillsField(value: unknown): { skills?: string[] } {
|
|
371
|
+
const skills = normalizeSkills(value);
|
|
372
|
+
return skills ? { skills } : {};
|
|
373
|
+
}
|
|
374
|
+
|
|
348
375
|
export function normalizeMcpTools(value: unknown): string[] {
|
|
349
376
|
return Array.isArray(value) ? [...new Set(value.filter((tool): tool is string => typeof tool === "string" && /^mcp__[A-Za-z0-9_]+__[A-Za-z0-9_]+$/.test(tool)))] : [];
|
|
350
377
|
}
|
|
@@ -353,20 +380,19 @@ export function normalizeMcpTools(value: unknown): string[] {
|
|
|
353
380
|
function normalizeAgent(base: AgentModelConfig, override: Partial<AgentModelConfig> | undefined): AgentModelConfig {
|
|
354
381
|
const merged = { ...base, ...(override ?? {}) };
|
|
355
382
|
const thinking = isThinkingLevel(merged.thinking) ? merged.thinking : base.thinking;
|
|
356
|
-
const timeoutMs = positive(merged.timeoutMs) ?? base.timeoutMs;
|
|
357
383
|
const mcpTools = normalizeMcpTools(merged.mcpTools);
|
|
358
|
-
return { model: merged.model, thinking, ...(mcpTools.length ? { mcpTools } : {}), ...(typeof merged.instructions === "string" ? { instructions: merged.instructions } : {}), ...(
|
|
384
|
+
return { model: merged.model, thinking, ...(mcpTools.length ? { mcpTools } : {}), ...skillsField(merged.skills), ...(typeof merged.instructions === "string" ? { instructions: merged.instructions } : {}), ...(merged.autoCompact === false ? { autoCompact: false } : {}) };
|
|
359
385
|
}
|
|
360
386
|
|
|
361
|
-
/** Scouts keep a model and a
|
|
387
|
+
/** Scouts keep a model and a tool-call cap; any thinking value in the file is dropped. */
|
|
362
388
|
function normalizeScout(override: Partial<ScoutConfig> | undefined): ScoutConfig {
|
|
363
389
|
const base = DEFAULT_CONFIG.scout;
|
|
364
390
|
const mcpTools = normalizeMcpTools(override?.mcpTools);
|
|
365
391
|
return {
|
|
366
392
|
model: typeof override?.model === "string" && override.model.trim() ? override.model : base.model,
|
|
367
|
-
timeoutMs: positive(override?.timeoutMs) ?? base.timeoutMs,
|
|
368
393
|
maxToolCalls: typeof override?.maxToolCalls === "number" && Number.isFinite(override.maxToolCalls) && override.maxToolCalls >= 0 ? Math.round(override.maxToolCalls) : base.maxToolCalls,
|
|
369
394
|
...(mcpTools.length ? { mcpTools } : {}),
|
|
395
|
+
...skillsField(override?.skills),
|
|
370
396
|
...(override?.autoCompact === false ? { autoCompact: false } : {}),
|
|
371
397
|
};
|
|
372
398
|
}
|
|
@@ -480,8 +506,14 @@ export function resolveConfig(partial: unknown): BotLobbyConfig {
|
|
|
480
506
|
workflow.routeQuickFixes = workflow.routeQuickFixes !== false;
|
|
481
507
|
workflow.repoMap = workflow.repoMap !== false;
|
|
482
508
|
workflow.reuseScouts = workflow.reuseScouts !== false;
|
|
509
|
+
workflow.parallelMode = isParallelMode(workflow.parallelMode) ? workflow.parallelMode : DEFAULT_CONFIG.workflow.parallelMode;
|
|
510
|
+
workflow.cleanReview = workflow.cleanReview !== false;
|
|
511
|
+
workflow.goalCheck = workflow.goalCheck !== false;
|
|
512
|
+
workflow.consultExpert = workflow.consultExpert !== false;
|
|
513
|
+
workflow.expertModel = typeof workflow.expertModel === "string" ? workflow.expertModel.trim() : "";
|
|
483
514
|
workflow.gitIsolation = isGitIsolation(workflow.gitIsolation) ? workflow.gitIsolation : DEFAULT_CONFIG.workflow.gitIsolation;
|
|
484
|
-
|
|
515
|
+
// Agent time limits are gone: a config file that still names them is cleaned on its next save.
|
|
516
|
+
for (const key of ["agentTimeoutMs", "wrapUpAt", "taskBudgetMinutes"]) delete (workflow as unknown as Record<string, unknown>)[key];
|
|
485
517
|
const knowledge = { ...DEFAULT_CONFIG.knowledge, ...(src.knowledge as Partial<KnowledgeConfig> | undefined) };
|
|
486
518
|
const srcAgents = (src.agents ?? {}) as Partial<BotLobbyConfig["agents"]>;
|
|
487
519
|
return {
|
|
@@ -509,15 +541,16 @@ export function hasScoutThinking(partial: unknown): boolean {
|
|
|
509
541
|
return Boolean(scout && typeof scout === "object" && "thinking" in scout);
|
|
510
542
|
}
|
|
511
543
|
|
|
512
|
-
/** What one subagent run uses: model (undefined = not configured), thinking and
|
|
544
|
+
/** What one subagent run uses: model (undefined = not configured), thinking and grants. */
|
|
513
545
|
export interface AgentProfile {
|
|
514
546
|
kind: WorkflowProfileKind;
|
|
515
547
|
model?: string;
|
|
516
548
|
thinking: string;
|
|
517
|
-
timeoutMs: number;
|
|
518
549
|
instructions?: string;
|
|
519
550
|
/** Exact native MCP tool names this profile may call; empty grants none. */
|
|
520
551
|
mcpTools?: string[];
|
|
552
|
+
/** Skill names it may load; absent keeps every skill pi finds. */
|
|
553
|
+
skills?: string[];
|
|
521
554
|
/** False when its settings turn auto-compaction off. */
|
|
522
555
|
autoCompact?: boolean;
|
|
523
556
|
}
|
|
@@ -527,6 +560,11 @@ function compactField(entry: { autoCompact?: boolean }): { autoCompact?: false }
|
|
|
527
560
|
return entry.autoCompact === false ? { autoCompact: false } : {};
|
|
528
561
|
}
|
|
529
562
|
|
|
563
|
+
/** The MCP tools and skills an entry grants, leaving out what it does not name. */
|
|
564
|
+
function grantFields(entry: { mcpTools?: string[]; skills?: string[] }): { mcpTools?: string[]; skills?: string[] } {
|
|
565
|
+
return { ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}), ...(entry.skills ? { skills: entry.skills } : {}) };
|
|
566
|
+
}
|
|
567
|
+
|
|
530
568
|
/** The settings entry a domain/role run draws from. */
|
|
531
569
|
export function profileKind(domain: Domain, role: Role): WorkflowProfileKind {
|
|
532
570
|
if (role === "scout") return "scout";
|
|
@@ -539,7 +577,7 @@ function modelOf(ref: ModelRef): string | undefined {
|
|
|
539
577
|
}
|
|
540
578
|
|
|
541
579
|
/**
|
|
542
|
-
* Resolve model, thinking and
|
|
580
|
+
* Resolve model, thinking and grants for one run from settings alone.
|
|
543
581
|
* Workers and the QA gate use their domain's entry, scouts and researchers
|
|
544
582
|
* their own; custom instructions always come from the domain, so a designer
|
|
545
583
|
* scout still carries the designer's house rules.
|
|
@@ -547,12 +585,11 @@ function modelOf(ref: ModelRef): string | undefined {
|
|
|
547
585
|
export function agentProfile(config: BotLobbyConfig, domain: Domain, role: Role): AgentProfile {
|
|
548
586
|
const kind = profileKind(domain, role);
|
|
549
587
|
const instructions = config.agents[domain].instructions;
|
|
550
|
-
const fallback = config.workflow.agentTimeoutMs;
|
|
551
588
|
if (kind === "scout") {
|
|
552
|
-
return { kind, model: modelOf(config.scout.model), thinking: SCOUT_THINKING,
|
|
589
|
+
return { kind, model: modelOf(config.scout.model), thinking: SCOUT_THINKING, instructions, ...grantFields(config.scout), ...compactField(config.scout) };
|
|
553
590
|
}
|
|
554
591
|
const entry = kind === "researcher" ? config.researcher : config.agents[kind];
|
|
555
|
-
return { kind, model: modelOf(entry.model), thinking: entry.thinking,
|
|
592
|
+
return { kind, model: modelOf(entry.model), thinking: entry.thinking, instructions, ...grantFields(entry), ...compactField(entry) };
|
|
556
593
|
}
|
|
557
594
|
|
|
558
595
|
/** The settings entry of a lobby agent (quick fix or planner). */
|
|
@@ -561,22 +598,27 @@ export function lobbyAgentConfig(config: BotLobbyConfig, kind: LobbyAgentKind):
|
|
|
561
598
|
}
|
|
562
599
|
|
|
563
600
|
/** Profile for a lobby agent run, from settings alone; `model` is undefined while unset. */
|
|
564
|
-
export function lobbyAgentProfile(config: BotLobbyConfig, kind: LobbyAgentKind): { model?: string; thinking: string;
|
|
601
|
+
export function lobbyAgentProfile(config: BotLobbyConfig, kind: LobbyAgentKind): { model?: string; thinking: string; instructions?: string; mcpTools?: string[]; skills?: string[]; autoCompact?: boolean } {
|
|
565
602
|
const entry = lobbyAgentConfig(config, kind);
|
|
566
|
-
return { model: modelOf(entry.model), thinking: entry.thinking,
|
|
603
|
+
return { model: modelOf(entry.model), thinking: entry.thinking, instructions: entry.instructions, ...grantFields(entry), ...compactField(entry) };
|
|
567
604
|
}
|
|
568
605
|
|
|
569
606
|
/**
|
|
570
607
|
* A planning panel seat's profile: the domain's (or the researcher's) model,
|
|
571
|
-
* thinking
|
|
608
|
+
* thinking, custom instructions and grants.
|
|
572
609
|
*/
|
|
573
|
-
export function panelMemberProfile(config: BotLobbyConfig, member: PanelMember): { model?: string; thinking: string;
|
|
610
|
+
export function panelMemberProfile(config: BotLobbyConfig, member: PanelMember): { model?: string; thinking: string; instructions?: string; mcpTools?: string[]; skills?: string[]; autoCompact?: boolean } {
|
|
574
611
|
const entry = member === "researcher" ? config.researcher : config.agents[member];
|
|
575
|
-
|
|
576
|
-
|
|
612
|
+
return { model: modelOf(entry.model), thinking: entry.thinking, instructions: entry.instructions, ...grantFields(entry), ...compactField(entry) };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** The model and thinking a worker's `ask_expert` consults: the configured expert, or the oracle's model. */
|
|
616
|
+
export function expertProfile(config: BotLobbyConfig): { model?: string; thinking: string } {
|
|
617
|
+
const model = config.workflow.expertModel ? modelOf(config.workflow.expertModel) : modelOf(config.master.model);
|
|
618
|
+
return { ...(model ? { model } : {}), thinking: "high" };
|
|
577
619
|
}
|
|
578
620
|
|
|
579
|
-
/** Resolves the model, thinking and
|
|
621
|
+
/** Resolves the model, thinking and grants one subagent run uses. */
|
|
580
622
|
export type ProfileResolver = (domain: Domain, role: Role) => AgentProfile;
|
|
581
623
|
|
|
582
624
|
/** The resolver a request carries, or plain settings when none was supplied (tests, headless use). */
|
package/src/schemas/findings.ts
CHANGED
|
@@ -54,10 +54,6 @@ export interface WorkerResult {
|
|
|
54
54
|
domain: Domain;
|
|
55
55
|
role: "worker";
|
|
56
56
|
completed: string;
|
|
57
|
-
/** Stopped for time: what it was doing and what is still to do. */
|
|
58
|
-
leftOff?: string;
|
|
59
|
-
/** Stopped for time: the minutes it asks for, and why. */
|
|
60
|
-
moreTime?: { minutes?: number; reason: string };
|
|
61
57
|
filesChanged: FileChange[];
|
|
62
58
|
verification: string;
|
|
63
59
|
notes: string;
|
|
@@ -157,8 +153,6 @@ export interface AgentRun {
|
|
|
157
153
|
thinking?: string;
|
|
158
154
|
/** Killed by the stall watchdog after going silent. */
|
|
159
155
|
stalled?: boolean;
|
|
160
|
-
/** Asked to wrap up before its deadline; the report may be partial. */
|
|
161
|
-
wrappedUp?: boolean;
|
|
162
156
|
/** File this worker is queued for at the file desk. */
|
|
163
157
|
waitingFor?: string;
|
|
164
158
|
/** The classifier routed this run down: the configured profile it came from (`p/big · medium`). */
|
|
@@ -167,10 +161,4 @@ export interface AgentRun {
|
|
|
167
161
|
route?: string;
|
|
168
162
|
/** Files the run changed with `edit`/`write`, absolute, across its attempts. */
|
|
169
163
|
edited?: string[];
|
|
170
|
-
/** Under a task time budget: the time it was given, when that runs out (epoch ms), and any granted since. */
|
|
171
|
-
allotMs?: number;
|
|
172
|
-
endsAt?: number;
|
|
173
|
-
extendedMs?: number;
|
|
174
|
-
/** Stopped at its allotted time and not given more: its report says where it left off. */
|
|
175
|
-
timeUp?: boolean;
|
|
176
164
|
}
|
package/src/schemas/task.ts
CHANGED
|
@@ -95,7 +95,6 @@ export interface RunLogEntry {
|
|
|
95
95
|
cost?: number;
|
|
96
96
|
attempts?: number;
|
|
97
97
|
stalled?: boolean;
|
|
98
|
-
wrappedUp?: boolean;
|
|
99
98
|
error?: string;
|
|
100
99
|
/** The classifier routed this run down from this configured profile. */
|
|
101
100
|
routedFrom?: string;
|
|
@@ -279,6 +278,18 @@ export interface Task {
|
|
|
279
278
|
lintWaiver?: { at: string };
|
|
280
279
|
/** Plan steps the user skipped, each with their reason; the oracle is told not to work on them. */
|
|
281
280
|
skippedSteps?: SkippedStep[];
|
|
281
|
+
/** The last goal check: the change (by its diff's hash) read against the user's request before completion. */
|
|
282
|
+
goalCheck?: GoalCheckRecord;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface GoalCheckRecord {
|
|
286
|
+
/** Hash of the diff it read: the same change is not checked twice. */
|
|
287
|
+
key: string;
|
|
288
|
+
verdict: "met" | "not_met";
|
|
289
|
+
gaps: string[];
|
|
290
|
+
at: string;
|
|
291
|
+
/** When completion was refused for these gaps: only then can the oracle complete anyway with a reason. */
|
|
292
|
+
refusedAt?: string;
|
|
282
293
|
}
|
|
283
294
|
|
|
284
295
|
export function createTask(
|
package/src/state/metrics.ts
CHANGED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A task's work clock. Work time counts while the oracle works on the task —
|
|
3
|
+
* its own turns, which include every agent it runs — and not while it waits
|
|
4
|
+
* on the user (between turns, and while a dialog is open). It is saved beside
|
|
5
|
+
* the task's state as work.json (see work-time.ts) by the session that owns
|
|
6
|
+
* the task. Nothing here limits how long an agent or a task may run.
|
|
7
|
+
*/
|
|
8
|
+
import { clearStaleRuns, updateWork } from "./work-time.ts";
|
|
9
|
+
|
|
10
|
+
/** `34m`, `1h 05m`. */
|
|
11
|
+
export function formatMinutes(ms: number): string {
|
|
12
|
+
const minutes = Math.max(0, Math.round(ms / 60_000));
|
|
13
|
+
if (minutes < 60) return `${minutes}m`;
|
|
14
|
+
return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, "0")}m`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* ------------------------------------------------------------ work clock */
|
|
18
|
+
/*
|
|
19
|
+
* Every task the session drives has a work clock: it keeps the task's work
|
|
20
|
+
* time (work.json, what the lobby shows as "worked"). It measures; it never
|
|
21
|
+
* limits how long anything runs.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
interface Clock {
|
|
25
|
+
root: string;
|
|
26
|
+
configDir: string;
|
|
27
|
+
/** The oracle's turn on the task is running. */
|
|
28
|
+
running: boolean;
|
|
29
|
+
/** Dialogs open: the user is being asked, so the clock waits. */
|
|
30
|
+
waits: number;
|
|
31
|
+
/** Since when the current stretch counts, while it does. */
|
|
32
|
+
since?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Work clocks of the tasks this process drives. */
|
|
36
|
+
const clocks = new Map<string, Clock>();
|
|
37
|
+
/** How often a running clock is saved, so other windows see time pass (and see it is still kept). */
|
|
38
|
+
export const CLOCK_FLUSH_MS = 30_000;
|
|
39
|
+
let flusher: ReturnType<typeof setInterval> | undefined;
|
|
40
|
+
/** The oracle's turn is running in this process. */
|
|
41
|
+
let turn = false;
|
|
42
|
+
|
|
43
|
+
/** Fold the stretch that counted into the saved totals, and start a new one if it still counts. */
|
|
44
|
+
function settle(taskId: string, clock: Clock, now = Date.now()): void {
|
|
45
|
+
let spent = 0;
|
|
46
|
+
if (clock.since !== undefined) {
|
|
47
|
+
spent = Math.max(0, now - clock.since);
|
|
48
|
+
clock.since = undefined;
|
|
49
|
+
}
|
|
50
|
+
if (clock.running && clock.waits === 0) clock.since = now;
|
|
51
|
+
const since = clock.since;
|
|
52
|
+
updateWork(clock.root, clock.configDir, taskId, (work) => {
|
|
53
|
+
work.workedMs += spent;
|
|
54
|
+
if (since !== undefined) work.runningSince = new Date(since).toISOString();
|
|
55
|
+
else delete work.runningSince;
|
|
56
|
+
}, new Date(now));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ensureFlusher(): void {
|
|
60
|
+
if (flusher) return;
|
|
61
|
+
flusher = setInterval(() => {
|
|
62
|
+
for (const [taskId, clock] of clocks) if (clock.running) settle(taskId, clock);
|
|
63
|
+
}, CLOCK_FLUSH_MS);
|
|
64
|
+
flusher.unref();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The oracle's turn started: a task it drives, or starts within the turn, is being worked on. */
|
|
68
|
+
export function turnStarted(): void {
|
|
69
|
+
turn = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The oracle started a turn on a task (or took one up within it): its clock runs. */
|
|
73
|
+
export function startClock(root: string, configDir: string, taskId: string): void {
|
|
74
|
+
let clock = clocks.get(taskId);
|
|
75
|
+
if (!clock) {
|
|
76
|
+
clock = { root, configDir, running: false, waits: 0 };
|
|
77
|
+
clocks.set(taskId, clock);
|
|
78
|
+
clearStaleRuns(root, configDir, taskId);
|
|
79
|
+
}
|
|
80
|
+
if (clock.running) return;
|
|
81
|
+
clock.running = true;
|
|
82
|
+
settle(taskId, clock);
|
|
83
|
+
ensureFlusher();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The task the session drives now, after a step that may have started, ended or taken over one: while
|
|
88
|
+
* a turn runs, its clock runs and any other task's stops.
|
|
89
|
+
*/
|
|
90
|
+
export function clockTask(root: string, configDir: string, taskId: string | undefined): void {
|
|
91
|
+
for (const [id, clock] of clocks) {
|
|
92
|
+
if (id === taskId || !clock.running) continue;
|
|
93
|
+
clock.running = false;
|
|
94
|
+
settle(id, clock);
|
|
95
|
+
}
|
|
96
|
+
if (taskId && turn) startClock(root, configDir, taskId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The oracle's turn ended: the clocks stop until the next one. */
|
|
100
|
+
export function stopClocks(): void {
|
|
101
|
+
turn = false;
|
|
102
|
+
for (const [taskId, clock] of clocks) {
|
|
103
|
+
if (!clock.running && clock.since === undefined) continue;
|
|
104
|
+
clock.running = false;
|
|
105
|
+
settle(taskId, clock);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The user is being asked: every clock waits (nested dialogs each call this and `resumeClocks`). */
|
|
110
|
+
export function pauseClocks(): void {
|
|
111
|
+
for (const [taskId, clock] of clocks) {
|
|
112
|
+
clock.waits += 1;
|
|
113
|
+
settle(taskId, clock);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function resumeClocks(): void {
|
|
118
|
+
for (const [taskId, clock] of clocks) {
|
|
119
|
+
clock.waits = Math.max(0, clock.waits - 1);
|
|
120
|
+
settle(taskId, clock);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Run `ask` (a dialog) with every clock stopped: time spent waiting on the user is not the task's. */
|
|
125
|
+
export async function whileAsking<T>(ask: () => Promise<T>): Promise<T> {
|
|
126
|
+
pauseClocks();
|
|
127
|
+
try {
|
|
128
|
+
return await ask();
|
|
129
|
+
} finally {
|
|
130
|
+
resumeClocks();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Forget the clocks (tests; a session that ends). */
|
|
135
|
+
export function resetClocks(): void {
|
|
136
|
+
clocks.clear();
|
|
137
|
+
turn = false;
|
|
138
|
+
if (flusher) clearInterval(flusher);
|
|
139
|
+
flusher = undefined;
|
|
140
|
+
}
|
package/src/state/work-time.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* How long a task's agents have actually worked on it, and which worker runs
|
|
3
3
|
* are under way. Work time is the oracle's turns on the task — which include
|
|
4
4
|
* every agent it runs — and not the time it waits on the user (between turns,
|
|
5
|
-
* and while a dialog is open); the work clock in
|
|
5
|
+
* and while a dialog is open); the work clock in work-clock.ts measures it. The
|
|
6
6
|
* session that owns the task keeps it beside the task's state as work.json,
|
|
7
7
|
* refreshed while it works, so every window can show the time ticking and
|
|
8
8
|
* mark the steps being worked on. A file its owner stopped refreshing (the
|
|
@@ -31,7 +31,6 @@ export interface WorkTime {
|
|
|
31
31
|
active: ActiveRun[];
|
|
32
32
|
/** When the owning session last wrote it. */
|
|
33
33
|
updatedAt: string;
|
|
34
|
-
stepLimits?: Record<string, { limitMs: number; usedMs: number }>;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
/** The clock as a reader sees it at one moment. */
|
|
@@ -51,16 +50,14 @@ export function workPath(root: string, configDir: string, taskId: string): strin
|
|
|
51
50
|
|
|
52
51
|
function isWork(value: unknown): value is WorkTime {
|
|
53
52
|
const work = value as Partial<WorkTime> | undefined;
|
|
54
|
-
return Boolean(work && Number.isFinite(work.workedMs) && typeof work.updatedAt === "string"
|
|
55
|
-
&& (work.stepLimits === undefined || (work.stepLimits !== null && typeof work.stepLimits === "object" && !Array.isArray(work.stepLimits)
|
|
56
|
-
&& Object.values(work.stepLimits).every((entry) => entry && Number.isFinite(entry.limitMs) && Number.isFinite(entry.usedMs) && entry.limitMs >= 0 && entry.usedMs >= 0))));
|
|
53
|
+
return Boolean(work && Number.isFinite(work.workedMs) && typeof work.updatedAt === "string");
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
export function readWork(root: string, configDir: string, taskId: string, strict = false): WorkTime | undefined {
|
|
60
57
|
try {
|
|
61
58
|
const value: unknown = JSON.parse(readFileSync(workPath(root, configDir, taskId), "utf8"));
|
|
62
59
|
if (!isWork(value)) throw new Error("Invalid task work record");
|
|
63
|
-
return { workedMs: value.workedMs, ...(value.runningSince ? { runningSince: value.runningSince } : {}), active: Array.isArray(value.active) ? value.active : [], updatedAt: value.updatedAt
|
|
60
|
+
return { workedMs: value.workedMs, ...(value.runningSince ? { runningSince: value.runningSince } : {}), active: Array.isArray(value.active) ? value.active : [], updatedAt: value.updatedAt };
|
|
64
61
|
} catch (error) {
|
|
65
62
|
if (strict && (error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
66
63
|
return undefined;
|
|
@@ -80,7 +77,7 @@ export function updateWork(root: string, configDir: string, taskId: string, chan
|
|
|
80
77
|
renameSync(tmp, path);
|
|
81
78
|
} catch (error) {
|
|
82
79
|
if (strict) throw error;
|
|
83
|
-
//
|
|
80
|
+
// A display clock may stop; it never holds up the work.
|
|
84
81
|
}
|
|
85
82
|
}
|
|
86
83
|
|
package/src/webui/api/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { plansDiscard, plansGet, plansStart } from "./plans.ts";
|
|
|
21
21
|
import { gitCancelReview, gitJev, gitPull, gitPulls, gitReview } from "./git.ts";
|
|
22
22
|
import { issuesCreate, issuesGet, issuesList } from "./issues.ts";
|
|
23
23
|
import { sessionsAnswer, sessionsArchive, sessionsChat, sessionsList, sessionsMessage, sessionsRename, sessionsStart, sessionsStop, sessionsSwitch } from "./sessions.ts";
|
|
24
|
-
import { settingsGet, settingsLinters, settingsSet } from "./settings.ts";
|
|
24
|
+
import { settingsGet, settingsGrants, settingsLinters, settingsSet } from "./settings.ts";
|
|
25
25
|
import { terminalClose, terminalInput, terminalList, terminalOpen, terminalResize } from "./terminal.ts";
|
|
26
26
|
import { MAX_INPUT_CHARS } from "../terminal.ts";
|
|
27
27
|
import { themesChoose, themesGet, themesRemove, themesRename, themesSave } from "./themes.ts";
|
|
@@ -385,6 +385,7 @@ function buildRoutes(): Record<string, Route> {
|
|
|
385
385
|
run: (body, ctx) => settingsSet(body as { patch: Record<string, unknown> }, ctx),
|
|
386
386
|
};
|
|
387
387
|
routes["settings.linters"] = { schema: Empty, run: (_body, ctx) => settingsLinters(ctx) };
|
|
388
|
+
routes["settings.grants"] = { schema: Empty, run: (_body, ctx) => settingsGrants(ctx) };
|
|
388
389
|
const ThemeId = Type.String({ minLength: 1, maxLength: 40 });
|
|
389
390
|
const ThemeName = Type.String({ minLength: 1, maxLength: 200 });
|
|
390
391
|
const ThemeVars = Type.Optional(Type.Record(Type.String({ maxLength: 60 }), Type.String({ maxLength: 400 })));
|
|
@@ -10,7 +10,7 @@ import { lobbyTopics } from "../../lobby/topics.ts";
|
|
|
10
10
|
import { prefillModels } from "../../pi/model-settings.ts";
|
|
11
11
|
import { kindLabel } from "../../pi/model-support.ts";
|
|
12
12
|
import { pushNotice } from "../notices.ts";
|
|
13
|
-
import type { SettingsInfo, SettingsLinter } from "../protocol.ts";
|
|
13
|
+
import type { SettingsGrants, SettingsInfo, SettingsLinter } from "../protocol.ts";
|
|
14
14
|
import type { ApiContext } from "./index.ts";
|
|
15
15
|
import { fail } from "./index.ts";
|
|
16
16
|
|
|
@@ -61,22 +61,6 @@ function checkPort(patch: Record<string, unknown>): void {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function agentEntries(patch: Record<string, unknown>): unknown[] {
|
|
65
|
-
const agents = isPlain(patch.agents) ? Object.values(patch.agents) : [];
|
|
66
|
-
return [patch.master, ...agents, patch.researcher, patch.quickFix, patch.planner, patch.scout];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Reject a time limit that is not a positive number, . */
|
|
70
|
-
function checkTimeouts(patch: Record<string, unknown>): void {
|
|
71
|
-
for (const entry of agentEntries(patch)) {
|
|
72
|
-
const timeout = isPlain(entry) ? entry.timeoutMs : undefined;
|
|
73
|
-
if (timeout === undefined) continue;
|
|
74
|
-
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) {
|
|
75
|
-
fail(400, "bad_request", `"${String(timeout)}" is not a positive number of minutes.`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
64
|
/**
|
|
81
65
|
* The page's first settings call. Subagents never inherit a model silently:
|
|
82
66
|
* any whose model is still unset is pinned to the session's model and saved,
|
|
@@ -99,10 +83,14 @@ export function settingsLinters(ctx: ApiContext): { linters: SettingsLinter[] }
|
|
|
99
83
|
return { linters: ctx.service.linters?.() ?? [] };
|
|
100
84
|
}
|
|
101
85
|
|
|
86
|
+
/** The MCP tools and skills pi offers now, for each agent's grants. */
|
|
87
|
+
export function settingsGrants(ctx: ApiContext): SettingsGrants {
|
|
88
|
+
return ctx.service.grants?.() ?? { mcpTools: [], skills: [] };
|
|
89
|
+
}
|
|
90
|
+
|
|
102
91
|
/** Merge, normalise and save the patch; the lobby rereads the config. */
|
|
103
92
|
export function settingsSet(body: { patch: Record<string, unknown> }, ctx: ApiContext): { config: BotLobbyConfig } {
|
|
104
93
|
checkPort(body.patch);
|
|
105
|
-
checkTimeouts(body.patch);
|
|
106
94
|
const config = stripSecrets(resolveConfig(mergeConfig(readConfig(ctx), body.patch)));
|
|
107
95
|
writeConfig(ctx, config);
|
|
108
96
|
ctx.service.configChanged?.();
|
|
@@ -409,6 +409,20 @@ export function createFixtureService(name: string, feed = new LobbyFeed()): Lobb
|
|
|
409
409
|
configChanged: () => {},
|
|
410
410
|
models: () => models.map((model) => ({ id: model.id, label: model.label, thinkingLevels: [...model.thinkingLevels] })),
|
|
411
411
|
classifierModels: () => [{ id: "opencode/jev-1.13-free", label: "Jev 1.13 (free)" }, { id: "openrouter/liquid/d1", label: "Liquid D1" }],
|
|
412
|
+
grants: () => ({
|
|
413
|
+
mcpTools: [
|
|
414
|
+
{ name: "mcp__github__get_issue", server: "github", description: "Read one issue with its comments.", readOnly: true },
|
|
415
|
+
{ name: "mcp__github__create_pull_request", server: "github", description: "Open a pull request.", readOnly: false },
|
|
416
|
+
{ name: "mcp__github__search_code", server: "github", description: "Search code across repositories.", readOnly: true },
|
|
417
|
+
{ name: "mcp__figma__get_frame", server: "figma", description: "Read a frame's layers and styles.", readOnly: true },
|
|
418
|
+
{ name: "mcp__figma__export_image", server: "figma", description: "Export a frame as PNG or SVG.", readOnly: false },
|
|
419
|
+
],
|
|
420
|
+
skills: [
|
|
421
|
+
{ name: "pdf-tools", description: "Extract text and tables from PDF files.", path: "~/.pi/agent/skills/pdf-tools/SKILL.md" },
|
|
422
|
+
{ name: "release-notes", description: "Write release notes from merged pull requests.", path: ".pi/skills/release-notes/SKILL.md" },
|
|
423
|
+
{ name: "tailwind-review", description: "Check Tailwind classes against the design tokens.", path: ".agents/skills/tailwind-review/SKILL.md" },
|
|
424
|
+
],
|
|
425
|
+
}),
|
|
412
426
|
issuesEnabled: () => fixture.status.issuesEnabled,
|
|
413
427
|
workspace: () => ({ ...fixture.status.workspace }),
|
|
414
428
|
zen: () => ({ ...(fixture.zen.task ? { task: { ...fixture.zen.task } } : {}), runs: [...fixture.zen.runs] }),
|
package/src/webui/protocol.ts
CHANGED
|
@@ -210,6 +210,28 @@ export interface SettingsClassifierModel {
|
|
|
210
210
|
label: string;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/** An MCP tool Settings can grant (`settings.grants`). */
|
|
214
|
+
export interface SettingsMcpTool {
|
|
215
|
+
name: string;
|
|
216
|
+
server: string;
|
|
217
|
+
description: string;
|
|
218
|
+
/** The server marks it read-only: read-only agents (scouts, QA, researcher) may call it. */
|
|
219
|
+
readOnly: boolean;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** A skill Settings can grant (`settings.grants`). */
|
|
223
|
+
export interface SettingsSkill {
|
|
224
|
+
name: string;
|
|
225
|
+
description: string;
|
|
226
|
+
path: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** `settings.grants`: what pi offers to grant agents now. */
|
|
230
|
+
export interface SettingsGrants {
|
|
231
|
+
mcpTools: SettingsMcpTool[];
|
|
232
|
+
skills: SettingsSkill[];
|
|
233
|
+
}
|
|
234
|
+
|
|
213
235
|
/** One message on the event stream (`GET /api/events`, server-sent events). */
|
|
214
236
|
export type StreamEvent =
|
|
215
237
|
| { type: "hello"; versions: Partial<Record<LobbyTopic, number>> }
|
|
@@ -606,6 +628,7 @@ export interface Api {
|
|
|
606
628
|
"settings.get": { request: Record<string, never>; result: SettingsInfo };
|
|
607
629
|
"settings.set": { request: { patch: Record<string, unknown> }; result: { config: BotLobbyConfig } };
|
|
608
630
|
"settings.linters": { request: Record<string, never>; result: { linters: SettingsLinter[] } };
|
|
631
|
+
"settings.grants": { request: Record<string, never>; result: SettingsGrants };
|
|
609
632
|
"themes.get": { request: Record<string, never>; result: ThemeStoreInfo };
|
|
610
633
|
"themes.save": { request: { name: string; light?: Record<string, string>; dark?: Record<string, string> }; result: ThemeStoreInfo & { id: string } };
|
|
611
634
|
"themes.rename": { request: { id: string; name: string }; result: ThemeStoreInfo };
|