@a-t-h-i/bot-lobby 0.6.18 → 0.6.20
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 +19 -1
- package/package.json +1 -1
- package/src/ask/relay.ts +3 -1
- package/src/ask/tool.ts +18 -3
- package/src/ask/types.ts +12 -0
- package/src/ask/web.ts +2 -1
- package/src/delivery/checks.ts +37 -0
- package/src/delivery/lock.ts +78 -0
- package/src/delivery/merge.ts +44 -0
- package/src/delivery/operations.ts +134 -0
- package/src/delivery/pr.ts +40 -0
- package/src/delivery/review.ts +90 -0
- package/src/delivery/rules.ts +46 -0
- package/src/delivery/source.ts +39 -0
- package/src/delivery/transport.ts +36 -0
- package/src/delivery/types.ts +60 -0
- package/src/execution/agent-runner.ts +1 -23
- package/src/index.ts +1 -1
- package/src/lobby/feed.ts +0 -6
- package/src/lobby/host.ts +3 -0
- package/src/lobby/planner.ts +1 -7
- package/src/lobby/pr-review.ts +1 -7
- package/src/lobby/prompt-hub.ts +7 -1
- package/src/lobby/quickfix.ts +3 -16
- package/src/lobby/service.ts +73 -3
- package/src/lobby/sessions.ts +12 -0
- package/src/lobby/task-rows.ts +5 -0
- package/src/master/master.ts +1 -1
- package/src/master/research.ts +0 -1
- package/src/pi/events.ts +0 -2
- package/src/pi/model-settings.ts +7 -26
- package/src/pi/model-support.ts +4 -19
- package/src/schemas/configuration.ts +8 -38
- package/src/schemas/findings.ts +0 -2
- package/src/schemas/task.ts +14 -0
- package/src/state/blocking-requests.ts +23 -0
- package/src/state/persistence.ts +12 -1
- package/src/state/phase-timing.ts +53 -0
- package/src/state/task-state.ts +2 -0
- package/src/state/timing-merge.ts +25 -0
- package/src/webui/api/index.ts +17 -1
- package/src/webui/api/status.ts +4 -2
- package/src/webui/api/tasks.ts +52 -0
- package/src/webui/protocol.ts +8 -0
- package/src/workflow/approvals.ts +3 -0
- package/src/workflow/workflow.ts +14 -33
- package/webui/dist/assets/index-9daVA-Sp.js +99 -0
- package/webui/dist/assets/index-DKVApIgy.css +2 -0
- package/webui/dist/build.json +142 -94
- package/webui/dist/index.html +4 -4
- package/webui/dist/manifest.webmanifest +2 -2
- package/webui/dist/offline.html +12 -9
- package/src/execution/fallback.ts +0 -75
- package/src/pi/master-fallback.ts +0 -61
- package/webui/dist/assets/index-CHLNxlA1.js +0 -98
- package/webui/dist/assets/index-m9XcQvH8.css +0 -2
package/src/master/research.ts
CHANGED
|
@@ -88,7 +88,6 @@ export async function runResearch(request: ResearchRequest, run: ProcessRunner =
|
|
|
88
88
|
model: profile.model,
|
|
89
89
|
thinking: profile.thinking,
|
|
90
90
|
timeoutMs: profile.timeoutMs,
|
|
91
|
-
...(profile.fallback ? { fallback: profile.fallback } : {}),
|
|
92
91
|
...(request.time ? { time: request.time } : {}),
|
|
93
92
|
cwd: request.cwd,
|
|
94
93
|
signal: request.signal,
|
package/src/pi/events.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { selectKnowledge } from "../knowledge/selector.ts";
|
|
|
7
7
|
import { cancelAllRuns } from "../execution/agent-runner.ts";
|
|
8
8
|
import { describeTask } from "../workflow/workflow.ts";
|
|
9
9
|
import { truncate } from "../text.ts";
|
|
10
|
-
import { registerMasterFallback } from "./master-fallback.ts";
|
|
11
10
|
import { applyStatus, clearStatus, isMinimized, setMinimized } from "./ui.ts";
|
|
12
11
|
import { isSubagentProcess, webToolsFor } from "./quiet.ts";
|
|
13
12
|
import { registerQuietTools } from "./tool-renderers.ts";
|
|
@@ -65,7 +64,6 @@ export function masterTaskContext(task: Task, comments: readonly PlanComment[] =
|
|
|
65
64
|
* section rather than replacing the whole prompt.
|
|
66
65
|
*/
|
|
67
66
|
export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
68
|
-
registerMasterFallback(pi);
|
|
69
67
|
// Web tools the oracle put away for the task in hand, given back when it ends.
|
|
70
68
|
let hiddenWeb: string[] = [];
|
|
71
69
|
pi.on("session_start", (_event, ctx) => {
|
package/src/pi/model-settings.ts
CHANGED
|
@@ -26,14 +26,11 @@ export interface EntryView {
|
|
|
26
26
|
thinking?: string;
|
|
27
27
|
instructions?: string;
|
|
28
28
|
timeoutMs?: number;
|
|
29
|
-
/** The model this agent switches to when its own runs out of usage; unset = none. */
|
|
30
|
-
fallbackModel?: string;
|
|
31
|
-
fallbackThinking?: string;
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
export function entryView(config: BotLobbyConfig, kind: SettingsKind): EntryView {
|
|
35
32
|
if (kind === "master") return config.master;
|
|
36
|
-
if (kind === "scout") return { model: config.scout.model, timeoutMs: config.scout.timeoutMs
|
|
33
|
+
if (kind === "scout") return { model: config.scout.model, timeoutMs: config.scout.timeoutMs };
|
|
37
34
|
if (kind === "researcher") return config.researcher;
|
|
38
35
|
if (kind === "quickfix") return config.quickFix;
|
|
39
36
|
if (kind === "planner") return config.planner;
|
|
@@ -45,33 +42,17 @@ interface EntryPatch {
|
|
|
45
42
|
thinking?: string;
|
|
46
43
|
instructions?: string;
|
|
47
44
|
timeoutMs?: number;
|
|
48
|
-
/** `inherit` clears the fallback (its thinking level with it). */
|
|
49
|
-
fallbackModel?: string;
|
|
50
|
-
fallbackThinking?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** A settings entry with the fallback patch applied: `inherit` removes the fallback model and its thinking. */
|
|
54
|
-
function withFallbackPatch<T extends { fallbackModel?: string; fallbackThinking?: string }>(entry: T): T {
|
|
55
|
-
if (entry.fallbackModel !== INHERIT_MODEL) return entry;
|
|
56
|
-
const { fallbackModel: _model, fallbackThinking: _thinking, ...rest } = entry;
|
|
57
|
-
return rest as T;
|
|
58
45
|
}
|
|
59
46
|
|
|
60
47
|
/** Apply a patch to one entry in a config copy; scouts ignore thinking and instructions. */
|
|
61
48
|
export function patchEntry(config: BotLobbyConfig, kind: SettingsKind, patch: EntryPatch): BotLobbyConfig {
|
|
62
49
|
const next: BotLobbyConfig = { ...config, agents: { ...config.agents } };
|
|
63
|
-
if (kind === "master") next.master =
|
|
64
|
-
else if (kind === "scout") {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
...(fallbackModel ? { fallbackModel } : {}),
|
|
70
|
-
});
|
|
71
|
-
} else if (kind === "researcher") next.researcher = withFallbackPatch({ ...config.researcher, ...patch } as AgentModelConfig);
|
|
72
|
-
else if (kind === "quickfix") next.quickFix = withFallbackPatch({ ...config.quickFix, ...patch } as AgentModelConfig);
|
|
73
|
-
else if (kind === "planner") next.planner = withFallbackPatch({ ...config.planner, ...patch } as AgentModelConfig);
|
|
74
|
-
else next.agents[kind] = withFallbackPatch({ ...config.agents[kind], ...patch } as AgentModelConfig);
|
|
50
|
+
if (kind === "master") next.master = { ...config.master, ...patch } as AgentModelConfig;
|
|
51
|
+
else if (kind === "scout") next.scout = { model: patch.model ?? config.scout.model, timeoutMs: patch.timeoutMs ?? config.scout.timeoutMs };
|
|
52
|
+
else if (kind === "researcher") next.researcher = { ...config.researcher, ...patch } as AgentModelConfig;
|
|
53
|
+
else if (kind === "quickfix") next.quickFix = { ...config.quickFix, ...patch } as AgentModelConfig;
|
|
54
|
+
else if (kind === "planner") next.planner = { ...config.planner, ...patch } as AgentModelConfig;
|
|
55
|
+
else next.agents[kind] = { ...config.agents[kind], ...patch } as AgentModelConfig;
|
|
75
56
|
return next;
|
|
76
57
|
}
|
|
77
58
|
|
package/src/pi/model-support.ts
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
type PanelMember,
|
|
9
9
|
THINKING_LEVELS,
|
|
10
10
|
type BotLobbyConfig,
|
|
11
|
-
type FallbackProfile,
|
|
12
11
|
type ProfileResolver,
|
|
13
12
|
type SubagentKind,
|
|
14
13
|
type ThinkingLevelName,
|
|
@@ -87,25 +86,11 @@ export function createProfileResolver(config: BotLobbyConfig, options: ResolverO
|
|
|
87
86
|
options.warn?.(message);
|
|
88
87
|
}
|
|
89
88
|
}
|
|
90
|
-
return { ...profile, model, thinking: check.level
|
|
89
|
+
return { ...profile, model, thinking: check.level };
|
|
91
90
|
};
|
|
92
91
|
}
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
function clampedFallback(fallback: FallbackProfile | undefined, label: string, options: ResolverOptions): { fallback?: FallbackProfile } {
|
|
96
|
-
if (!fallback) return {};
|
|
97
|
-
const check = checkThinking(options.lookup(fallback.model), fallback.thinking);
|
|
98
|
-
if (check.warning) {
|
|
99
|
-
const message = `bot-lobby: ${label} fallback — ${check.warning}. Change it in /bot-lobby settings.`;
|
|
100
|
-
if (!warned.has(message)) {
|
|
101
|
-
warned.add(message);
|
|
102
|
-
options.warn?.(message);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return { fallback: { model: fallback.model, thinking: check.level } };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
type RunProfile = { model?: string; thinking: string; timeoutMs: number; instructions?: string; fallback?: FallbackProfile };
|
|
93
|
+
type RunProfile = { model?: string; thinking: string; timeoutMs: number; instructions?: string };
|
|
109
94
|
|
|
110
95
|
/** Unset models run on the session's; thinking is clamped to the model, with a one-time warning. */
|
|
111
96
|
function resolveRunProfile(profile: RunProfile, label: string, options: ResolverOptions): RunProfile {
|
|
@@ -118,7 +103,7 @@ function resolveRunProfile(profile: RunProfile, label: string, options: Resolver
|
|
|
118
103
|
options.warn?.(message);
|
|
119
104
|
}
|
|
120
105
|
}
|
|
121
|
-
return { ...profile, model, thinking: check.level
|
|
106
|
+
return { ...profile, model, thinking: check.level };
|
|
122
107
|
}
|
|
123
108
|
|
|
124
109
|
/** A lobby agent's model, thinking and time limit. */
|
|
@@ -137,7 +122,7 @@ export function resolvePanelProfile(config: BotLobbyConfig, member: PanelMember,
|
|
|
137
122
|
*/
|
|
138
123
|
export function resolveReviewProfile(config: BotLobbyConfig, options: ResolverOptions): RunProfile {
|
|
139
124
|
const profile = agentProfile(config, "qa", "reviewer");
|
|
140
|
-
return resolveRunProfile({ model: profile.model, thinking: profile.thinking, timeoutMs: profile.timeoutMs, ...(profile.instructions ? { instructions: profile.instructions } : {})
|
|
125
|
+
return resolveRunProfile({ model: profile.model, thinking: profile.thinking, timeoutMs: profile.timeoutMs, ...(profile.instructions ? { instructions: profile.instructions } : {}) }, kindLabel("qa"), options);
|
|
141
126
|
}
|
|
142
127
|
|
|
143
128
|
/** Every configured subagent whose thinking level its model does not support, for `/bot-lobby config`. */
|
|
@@ -31,18 +31,12 @@ export interface AgentModelConfig {
|
|
|
31
31
|
instructions?: string;
|
|
32
32
|
/** Time limit per run; falls back to `workflow.agentTimeoutMs`. */
|
|
33
33
|
timeoutMs?: number;
|
|
34
|
-
/** Model this agent switches to when its own model runs out of usage or is unavailable; unset or `inherit` = none. */
|
|
35
|
-
fallbackModel?: ModelRef;
|
|
36
|
-
/** Thinking level on the fallback model; unset uses this agent's own `thinking`. */
|
|
37
|
-
fallbackThinking?: string;
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
/** Scouts pick a model and a time limit only; their thinking is fixed at `SCOUT_THINKING`. */
|
|
41
37
|
export interface ScoutConfig {
|
|
42
38
|
model: ModelRef;
|
|
43
39
|
timeoutMs: number;
|
|
44
|
-
/** Model scouts switch to when theirs runs out of usage or is unavailable (they think at `SCOUT_THINKING` on it too). */
|
|
45
|
-
fallbackModel?: ModelRef;
|
|
46
40
|
}
|
|
47
41
|
|
|
48
42
|
/** Subagent kinds with their own settings entry. */
|
|
@@ -301,22 +295,18 @@ function positive(value: unknown): number | undefined {
|
|
|
301
295
|
|
|
302
296
|
/** Merge one agent's override over its default; legacy `inherit` or unknown thinking falls back to the default level. */
|
|
303
297
|
function normalizeAgent(base: AgentModelConfig, override: Partial<AgentModelConfig> | undefined): AgentModelConfig {
|
|
304
|
-
const
|
|
298
|
+
const merged = { ...base, ...(override ?? {}) };
|
|
305
299
|
const thinking = isThinkingLevel(merged.thinking) ? merged.thinking : base.thinking;
|
|
306
300
|
const timeoutMs = positive(merged.timeoutMs) ?? base.timeoutMs;
|
|
307
|
-
|
|
308
|
-
const fallbackThinking = typeof rawThinking === "string" && isThinkingLevel(rawThinking) ? rawThinking : undefined;
|
|
309
|
-
return { ...merged, thinking, ...(timeoutMs ? { timeoutMs } : {}), ...(fallbackModel ? { fallbackModel, ...(fallbackThinking ? { fallbackThinking } : {}) } : {}) };
|
|
301
|
+
return { model: merged.model, thinking, ...(typeof merged.instructions === "string" ? { instructions: merged.instructions } : {}), ...(timeoutMs ? { timeoutMs } : {}) };
|
|
310
302
|
}
|
|
311
303
|
|
|
312
304
|
/** Scouts keep a model and a time limit; any thinking value in the file is dropped. */
|
|
313
305
|
function normalizeScout(override: Partial<ScoutConfig> | undefined): ScoutConfig {
|
|
314
306
|
const base = DEFAULT_CONFIG.scout;
|
|
315
|
-
const fallback = override?.fallbackModel;
|
|
316
307
|
return {
|
|
317
308
|
model: typeof override?.model === "string" && override.model.trim() ? override.model : base.model,
|
|
318
309
|
timeoutMs: positive(override?.timeoutMs) ?? base.timeoutMs,
|
|
319
|
-
...(typeof fallback === "string" && fallback.trim() && fallback !== INHERIT_MODEL ? { fallbackModel: fallback.trim() } : {}),
|
|
320
310
|
};
|
|
321
311
|
}
|
|
322
312
|
|
|
@@ -441,22 +431,6 @@ export interface AgentProfile {
|
|
|
441
431
|
thinking: string;
|
|
442
432
|
timeoutMs: number;
|
|
443
433
|
instructions?: string;
|
|
444
|
-
/** Where the run goes when its model runs out of usage or is unavailable; absent = it just fails. */
|
|
445
|
-
fallback?: FallbackProfile;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/** The fallback model and the thinking level to run it at. */
|
|
449
|
-
export interface FallbackProfile {
|
|
450
|
-
model: string;
|
|
451
|
-
thinking: string;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/** The fallback of a settings entry: its model and thinking (the entry's own level when none is set). */
|
|
455
|
-
export function fallbackOf(entry: { fallbackModel?: string; fallbackThinking?: string; thinking?: string }): FallbackProfile | undefined {
|
|
456
|
-
const model = modelOf(entry.fallbackModel ?? INHERIT_MODEL);
|
|
457
|
-
if (!model) return undefined;
|
|
458
|
-
const own = entry.thinking && isThinkingLevel(entry.thinking) ? entry.thinking : DEFAULT_THINKING;
|
|
459
|
-
return { model, thinking: entry.fallbackThinking && isThinkingLevel(entry.fallbackThinking) ? entry.fallbackThinking : own };
|
|
460
434
|
}
|
|
461
435
|
|
|
462
436
|
/** The settings entry a domain/role run draws from. */
|
|
@@ -481,12 +455,10 @@ export function agentProfile(config: BotLobbyConfig, domain: Domain, role: Role)
|
|
|
481
455
|
const instructions = config.agents[domain].instructions;
|
|
482
456
|
const fallback = config.workflow.agentTimeoutMs;
|
|
483
457
|
if (kind === "scout") {
|
|
484
|
-
|
|
485
|
-
return { kind, model: modelOf(config.scout.model), thinking: SCOUT_THINKING, timeoutMs: config.scout.timeoutMs || fallback, instructions, ...(scoutFallback ? { fallback: scoutFallback } : {}) };
|
|
458
|
+
return { kind, model: modelOf(config.scout.model), thinking: SCOUT_THINKING, timeoutMs: config.scout.timeoutMs || fallback, instructions };
|
|
486
459
|
}
|
|
487
460
|
const entry = kind === "researcher" ? config.researcher : config.agents[kind];
|
|
488
|
-
|
|
489
|
-
return { kind, model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? fallback, instructions, ...(entryFallback ? { fallback: entryFallback } : {}) };
|
|
461
|
+
return { kind, model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? fallback, instructions };
|
|
490
462
|
}
|
|
491
463
|
|
|
492
464
|
/** The settings entry of a lobby agent (quick fix or planner). */
|
|
@@ -495,21 +467,19 @@ export function lobbyAgentConfig(config: BotLobbyConfig, kind: LobbyAgentKind):
|
|
|
495
467
|
}
|
|
496
468
|
|
|
497
469
|
/** Profile for a lobby agent run, from settings alone; `model` is undefined while unset. */
|
|
498
|
-
export function lobbyAgentProfile(config: BotLobbyConfig, kind: LobbyAgentKind): { model?: string; thinking: string; timeoutMs: number; instructions?: string
|
|
470
|
+
export function lobbyAgentProfile(config: BotLobbyConfig, kind: LobbyAgentKind): { model?: string; thinking: string; timeoutMs: number; instructions?: string } {
|
|
499
471
|
const entry = lobbyAgentConfig(config, kind);
|
|
500
|
-
|
|
501
|
-
return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? config.workflow.agentTimeoutMs, instructions: entry.instructions, ...(fallback ? { fallback } : {}) };
|
|
472
|
+
return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? config.workflow.agentTimeoutMs, instructions: entry.instructions };
|
|
502
473
|
}
|
|
503
474
|
|
|
504
475
|
/**
|
|
505
476
|
* A planning panel seat's profile: the domain's (or the researcher's) model,
|
|
506
477
|
* thinking and custom instructions, bounded by the planner's per-turn limit.
|
|
507
478
|
*/
|
|
508
|
-
export function panelMemberProfile(config: BotLobbyConfig, member: PanelMember): { model?: string; thinking: string; timeoutMs: number; instructions?: string
|
|
479
|
+
export function panelMemberProfile(config: BotLobbyConfig, member: PanelMember): { model?: string; thinking: string; timeoutMs: number; instructions?: string } {
|
|
509
480
|
const entry = member === "researcher" ? config.researcher : config.agents[member];
|
|
510
481
|
const timeoutMs = config.planner.timeoutMs ?? config.workflow.agentTimeoutMs;
|
|
511
|
-
|
|
512
|
-
return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs, instructions: entry.instructions, ...(fallback ? { fallback } : {}) };
|
|
482
|
+
return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs, instructions: entry.instructions };
|
|
513
483
|
}
|
|
514
484
|
|
|
515
485
|
/** Resolves the model, thinking and time limit one subagent run uses. */
|
package/src/schemas/findings.ts
CHANGED
|
@@ -139,8 +139,6 @@ export interface AgentRun {
|
|
|
139
139
|
wrappedUp?: boolean;
|
|
140
140
|
/** File this worker is queued for at the file desk. */
|
|
141
141
|
waitingFor?: string;
|
|
142
|
-
/** The run switched to its fallback model because this one ran out of usage or was unavailable. */
|
|
143
|
-
fellBackFrom?: string;
|
|
144
142
|
/** The classifier routed this run down: the configured profile it came from (`p/big · medium`). */
|
|
145
143
|
routedFrom?: string;
|
|
146
144
|
/** Why and where it was routed (`trivial 0.88: p/big · medium → p/cheap · low`). */
|
package/src/schemas/task.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Domain, Role } from "./agent.ts";
|
|
2
|
+
import type { Delivery } from "../delivery/types.ts";
|
|
2
3
|
|
|
3
4
|
export const TASK_STATES = [
|
|
4
5
|
"created",
|
|
@@ -171,7 +172,18 @@ export interface TaskGit {
|
|
|
171
172
|
path?: string;
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
export interface PhaseTiming {
|
|
176
|
+
phase: TaskState;
|
|
177
|
+
elapsedMs: number;
|
|
178
|
+
runningSince?: string;
|
|
179
|
+
blockingRequestIds: string[];
|
|
180
|
+
}
|
|
181
|
+
|
|
174
182
|
export interface Task {
|
|
183
|
+
/** Canonical waits, including legacy tasks without a reliable clock. */
|
|
184
|
+
blockingRequestIds?: string[];
|
|
185
|
+
phaseTiming?: PhaseTiming;
|
|
186
|
+
delivery?: Delivery;
|
|
175
187
|
id: string;
|
|
176
188
|
title: string;
|
|
177
189
|
/** The user's original request, kept in full while `title` stays a short label. */
|
|
@@ -226,6 +238,8 @@ export function createTask(
|
|
|
226
238
|
title,
|
|
227
239
|
request,
|
|
228
240
|
state: "created",
|
|
241
|
+
blockingRequestIds: [],
|
|
242
|
+
phaseTiming: { phase: "created", elapsedMs: 0, runningSince: now, blockingRequestIds: [] },
|
|
229
243
|
domains: [],
|
|
230
244
|
amendments: [],
|
|
231
245
|
paused: false,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { loadTask, saveTask } from "./persistence.ts";
|
|
4
|
+
import { blockPhaseTiming, resolvePhaseTiming } from "./phase-timing.ts";
|
|
5
|
+
|
|
6
|
+
export interface TaskRequestIdentity { root: string; configDir: string; taskId: string; sessionId?: string }
|
|
7
|
+
export const blockingRequestContext = new AsyncLocalStorage<TaskRequestIdentity & { requestId: string }>();
|
|
8
|
+
|
|
9
|
+
/** Always mutate a fresh authoritative task, never the workflow's long-lived copy. */
|
|
10
|
+
export function updateBlockingRequest(identity: TaskRequestIdentity, id: string, pending: boolean): void {
|
|
11
|
+
const task = loadTask(identity.root, identity.configDir, identity.taskId);
|
|
12
|
+
if (!task || (identity.sessionId && task.ownerSessionId !== identity.sessionId)) return;
|
|
13
|
+
if (pending) blockPhaseTiming(task, id);
|
|
14
|
+
else resolvePhaseTiming(task, id);
|
|
15
|
+
saveTask(identity.root, identity.configDir, task);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function withBlockingRequest<T>(identity: TaskRequestIdentity, run: () => Promise<T>): Promise<T> {
|
|
19
|
+
const id = `request:${randomUUID()}`;
|
|
20
|
+
updateBlockingRequest(identity, id, true);
|
|
21
|
+
try { return await blockingRequestContext.run({ ...identity, requestId: id }, run); }
|
|
22
|
+
finally { updateBlockingRequest(identity, id, false); }
|
|
23
|
+
}
|
package/src/state/persistence.ts
CHANGED
|
@@ -16,6 +16,13 @@ import {
|
|
|
16
16
|
import { DEFAULT_KNOWLEDGE_CONTENT, ensureFile, readFileOr, writeFileEnsured } from "../knowledge/store.ts";
|
|
17
17
|
import { forgetCached, forgetCachedUnder, readJsonCached } from "./file-cache.ts";
|
|
18
18
|
import { taskName } from "../text.ts";
|
|
19
|
+
import { mergePhaseTiming, timingBaseline, type TimingBaseline } from "./timing-merge.ts";
|
|
20
|
+
|
|
21
|
+
const timingBaselines = new WeakMap<Task, TimingBaseline>();
|
|
22
|
+
function rememberTiming(task: Task): Task {
|
|
23
|
+
timingBaselines.set(task, timingBaseline(task));
|
|
24
|
+
return task;
|
|
25
|
+
}
|
|
19
26
|
|
|
20
27
|
/** Idempotently create the full knowledge + tasks layout with seed files. */
|
|
21
28
|
export function ensureProjectStructure(root: string, configDir: string): void {
|
|
@@ -40,6 +47,7 @@ export function createTaskDir(root: string, configDir: string, task: Task): void
|
|
|
40
47
|
mkdirSync(dir, { recursive: true });
|
|
41
48
|
forgetCached(join(dir, "state.json"));
|
|
42
49
|
writeFileEnsured(join(dir, "state.json"), JSON.stringify(task, null, 2));
|
|
50
|
+
rememberTiming(task);
|
|
43
51
|
ensureFile(join(dir, "proposal.md"), "");
|
|
44
52
|
ensureFile(join(dir, "plan.md"), "");
|
|
45
53
|
for (const domain of ["designer", "backend", "qa"] as const) {
|
|
@@ -80,9 +88,12 @@ export function readTaskArtifact(
|
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
export function saveTask(root: string, configDir: string, task: Task): void {
|
|
91
|
+
const latest = loadTask(root, configDir, task.id);
|
|
92
|
+
if (latest) mergePhaseTiming(task, timingBaselines.get(task), latest);
|
|
83
93
|
const path = join(taskDir(dataRoot(root, configDir), task.id), "state.json");
|
|
84
94
|
forgetCached(path);
|
|
85
95
|
writeFileEnsured(path, JSON.stringify(task, null, 2));
|
|
96
|
+
rememberTiming(task);
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
/** Read a task state: the bot-lobby copy wins, else the newest pre-rename copy. */
|
|
@@ -91,7 +102,7 @@ export function loadTask(root: string, configDir: string, taskId: string): Task
|
|
|
91
102
|
const path = join(taskDir(dr, taskId), "state.json");
|
|
92
103
|
if (!existsSync(path)) continue;
|
|
93
104
|
try {
|
|
94
|
-
return JSON.parse(readFileSync(path, "utf8")) as Task;
|
|
105
|
+
return rememberTiming(JSON.parse(readFileSync(path, "utf8")) as Task);
|
|
95
106
|
} catch {
|
|
96
107
|
// A bot-lobby state.json that exists but cannot be parsed is surfaced, not shadowed.
|
|
97
108
|
return undefined;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { TERMINAL_STATES, type Task, type TaskState } from "../schemas/task.ts";
|
|
2
|
+
|
|
3
|
+
export interface TimingProjection {
|
|
4
|
+
phase: string;
|
|
5
|
+
elapsedMs: number;
|
|
6
|
+
runningSince?: string;
|
|
7
|
+
waiting: boolean;
|
|
8
|
+
serverNow: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function elapsed(task: Task, now: string): number {
|
|
12
|
+
const timing = task.phaseTiming;
|
|
13
|
+
if (!timing) return 0;
|
|
14
|
+
const delta = timing.runningSince ? Date.parse(now) - Date.parse(timing.runningSince) : 0;
|
|
15
|
+
return timing.elapsedMs + (Number.isFinite(delta) ? Math.max(0, delta) : 0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resetPhaseTiming(task: Task, phase: TaskState, now: string): void {
|
|
19
|
+
const blockingRequestIds = [...(task.blockingRequestIds ?? task.phaseTiming?.blockingRequestIds ?? [])];
|
|
20
|
+
task.blockingRequestIds = [...blockingRequestIds];
|
|
21
|
+
task.phaseTiming = { phase, elapsedMs: 0, blockingRequestIds };
|
|
22
|
+
if (!blockingRequestIds.length && !TERMINAL_STATES.includes(phase)) task.phaseTiming.runningSince = now;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Legacy tasks remain unavailable until a real phase transition. */
|
|
26
|
+
export function blockPhaseTiming(task: Task, id: string, now = new Date().toISOString()): void {
|
|
27
|
+
const ids = task.blockingRequestIds ?? task.phaseTiming?.blockingRequestIds ?? [];
|
|
28
|
+
if (ids.includes(id)) return;
|
|
29
|
+
task.blockingRequestIds = [...ids, id];
|
|
30
|
+
const timing = task.phaseTiming;
|
|
31
|
+
if (!timing) return;
|
|
32
|
+
timing.elapsedMs = elapsed(task, now);
|
|
33
|
+
delete timing.runningSince;
|
|
34
|
+
timing.blockingRequestIds = [...task.blockingRequestIds];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolvePhaseTiming(task: Task, id: string, now = new Date().toISOString()): void {
|
|
38
|
+
const ids = task.blockingRequestIds ?? task.phaseTiming?.blockingRequestIds ?? [];
|
|
39
|
+
if (!ids.includes(id)) return;
|
|
40
|
+
task.blockingRequestIds = ids.filter((entry) => entry !== id);
|
|
41
|
+
const timing = task.phaseTiming;
|
|
42
|
+
if (!timing) return;
|
|
43
|
+
timing.blockingRequestIds = [...task.blockingRequestIds];
|
|
44
|
+
if (!timing.blockingRequestIds.length && !TERMINAL_STATES.includes(task.state)) timing.runningSince = now;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Baseline plus anchor: clients advance only the anchor, not server wall time. */
|
|
48
|
+
export function projectPhaseTiming(task: Task, serverNow = new Date().toISOString()): TimingProjection | undefined {
|
|
49
|
+
const timing = task.phaseTiming;
|
|
50
|
+
if (!timing) return undefined;
|
|
51
|
+
return { phase: timing.phase, elapsedMs: timing.elapsedMs, runningSince: timing.runningSince,
|
|
52
|
+
waiting: timing.blockingRequestIds.length > 0, serverNow };
|
|
53
|
+
}
|
package/src/state/task-state.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Task, TaskState } from "../schemas/task.ts";
|
|
2
2
|
import { assertTransition } from "../workflow/transitions.ts";
|
|
3
|
+
import { resetPhaseTiming } from "./phase-timing.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Single mutation point for task state. Every state change flows through here
|
|
@@ -16,6 +17,7 @@ export function onTransition(fn: TransitionListener): void {
|
|
|
16
17
|
|
|
17
18
|
export function transition(task: Task, to: TaskState, now = new Date().toISOString()): void {
|
|
18
19
|
assertTransition(task.state, to);
|
|
20
|
+
resetPhaseTiming(task, to, now);
|
|
19
21
|
task.state = to;
|
|
20
22
|
task.updatedAt = now;
|
|
21
23
|
listener?.(task, to);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PhaseTiming, Task } from "../schemas/task.ts";
|
|
2
|
+
import { blockPhaseTiming, resetPhaseTiming, resolvePhaseTiming } from "./phase-timing.ts";
|
|
3
|
+
|
|
4
|
+
export interface TimingBaseline { timing?: PhaseTiming; ids: string[] }
|
|
5
|
+
export function timingBaseline(task: Task): TimingBaseline {
|
|
6
|
+
return { timing: task.phaseTiming ? structuredClone(task.phaseTiming) : undefined,
|
|
7
|
+
ids: [...(task.blockingRequestIds ?? task.phaseTiming?.blockingRequestIds ?? [])] };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Apply only this copy's wait deltas to the authoritative registry and clock. */
|
|
11
|
+
export function mergePhaseTiming(task: Task, baseline: TimingBaseline | undefined, latest: Task): void {
|
|
12
|
+
const incoming = timingBaseline(task);
|
|
13
|
+
const oldIds = baseline?.ids ?? incoming.ids;
|
|
14
|
+
task.blockingRequestIds = [...(latest.blockingRequestIds ?? latest.phaseTiming?.blockingRequestIds ?? [])];
|
|
15
|
+
if (JSON.stringify(baseline?.timing) !== JSON.stringify(latest.phaseTiming)) {
|
|
16
|
+
task.phaseTiming = latest.phaseTiming ? structuredClone(latest.phaseTiming) : undefined;
|
|
17
|
+
}
|
|
18
|
+
const now = new Date().toISOString();
|
|
19
|
+
for (const id of oldIds.filter((id) => !incoming.ids.includes(id))) resolvePhaseTiming(task, id, now);
|
|
20
|
+
for (const id of incoming.ids.filter((id) => !oldIds.includes(id))) blockPhaseTiming(task, id, now);
|
|
21
|
+
if (task.phaseTiming) task.phaseTiming.blockingRequestIds = [...task.blockingRequestIds];
|
|
22
|
+
if (task.state !== latest.state || (task.phaseTiming && task.state !== task.phaseTiming.phase)) {
|
|
23
|
+
resetPhaseTiming(task, task.state, task.updatedAt);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/webui/api/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { lobbyAbort, lobbyHistory, lobbySend, lobbySnapshot } from "./lobby.ts";
|
|
|
15
15
|
import { promptsAnswer, promptsDismiss, promptsList } from "./prompts.ts";
|
|
16
16
|
import { plannerAnswer, plannerCommentLine, plannerGet, plannerNew, plannerRetry, plannerSend, plannerEditMessage, plannerToggleSeat, plannerSave } from "./planner.ts";
|
|
17
17
|
import { quickfixCancel, quickfixList, quickfixMovedToTask, quickfixRunAnyway, quickfixSubmit } from "./quickfix.ts";
|
|
18
|
-
import { tasksArchive, tasksArchived, tasksAuto, tasksComment, tasksComments, tasksDelete, tasksEditComment, tasksGet, tasksList, tasksMessage, tasksRestore } from "./tasks.ts";
|
|
18
|
+
import { tasksArchive, tasksArchived, tasksAuto, tasksComment, tasksComments, tasksDelete, tasksEditComment, tasksGet, tasksList, tasksMessage, tasksRestore, tasksOpen, tasksDeliveryReview, tasksDeliveryDefer, tasksDeliver } from "./tasks.ts";
|
|
19
19
|
import { metricsGet } from "./metrics.ts";
|
|
20
20
|
import { knowledgeAdd, knowledgeComment, knowledgeEdit, knowledgeFiles, knowledgeOpen, knowledgeRemove, knowledgeReplaceFile, knowledgeUnnote } from "./knowledge.ts";
|
|
21
21
|
import { excalidrawAdd, excalidrawCheck, excalidrawCreate, excalidrawList, excalidrawRemove, excalidrawRename, excalidrawReveal, excalidrawToggleAgent, excalidrawToggleAll, excalidrawToggleContribute } from "./excalidraw.ts";
|
|
@@ -124,6 +124,22 @@ function buildRoutes(): Record<string, Route> {
|
|
|
124
124
|
schema: Type.Object({ taskId: TaskId }, { additionalProperties: false }),
|
|
125
125
|
run: (body, ctx) => tasksGet(body as { taskId: string }, ctx),
|
|
126
126
|
};
|
|
127
|
+
routes["tasks.open"] = {
|
|
128
|
+
schema: Type.Object({ taskId: TaskId }, { additionalProperties: false }),
|
|
129
|
+
run: (body, ctx) => tasksOpen(body as { taskId: string }, ctx),
|
|
130
|
+
};
|
|
131
|
+
routes["tasks.deliveryReview"] = {
|
|
132
|
+
schema: Type.Object({ taskId: TaskId }, { additionalProperties: false }),
|
|
133
|
+
run: (body, ctx) => tasksDeliveryReview(body as { taskId: string }, ctx),
|
|
134
|
+
};
|
|
135
|
+
routes["tasks.deliver"] = {
|
|
136
|
+
schema: Type.Object({ taskId: TaskId, reviewId: Type.String({ minLength: 1, maxLength: 200 }), action: Type.Union([Type.Literal("create_pr"), Type.Literal("merge_main")]), confirmMain: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
137
|
+
run: (body, ctx) => tasksDeliver(body as unknown as Parameters<typeof tasksDeliver>[0], ctx),
|
|
138
|
+
};
|
|
139
|
+
routes["tasks.deliveryDefer"] = {
|
|
140
|
+
schema: Type.Object({ taskId: TaskId, reviewId: Type.String({ minLength: 1, maxLength: 200 }) }, { additionalProperties: false }),
|
|
141
|
+
run: (body, ctx) => tasksDeliveryDefer(body as { taskId: string; reviewId: string }, ctx),
|
|
142
|
+
};
|
|
127
143
|
routes["tasks.comments"] = {
|
|
128
144
|
schema: Type.Object({ taskId: TaskId }, { additionalProperties: false }),
|
|
129
145
|
run: (body, ctx) => tasksComments(body as { taskId: string }, ctx),
|
package/src/webui/api/status.ts
CHANGED
|
@@ -28,6 +28,8 @@ export function statusGet(ctx: ApiContext): StatusInfo {
|
|
|
28
28
|
const service = ctx.service;
|
|
29
29
|
const issuesEnabled = service.issuesEnabled();
|
|
30
30
|
const workspace = service.workspace?.() ?? { name: "bot-lobby" };
|
|
31
|
+
// The service's own config when it has one, so a saved panes or keys change shows without a restart.
|
|
32
|
+
const config = service.config?.() ?? loadConfig();
|
|
31
33
|
return {
|
|
32
34
|
workspace: { name: workspace.name, ...(workspace.branch ? { branch: workspace.branch } : {}) },
|
|
33
35
|
...(workspace.branch ? { branch: workspace.branch } : {}),
|
|
@@ -36,9 +38,9 @@ export function statusGet(ctx: ApiContext): StatusInfo {
|
|
|
36
38
|
busy: service.masterBusy(),
|
|
37
39
|
port: ctx.port,
|
|
38
40
|
issuesEnabled,
|
|
39
|
-
panels:
|
|
41
|
+
panels: config.lobby.panels,
|
|
40
42
|
tabs: tabs(issuesEnabled),
|
|
41
|
-
keys: keys(
|
|
43
|
+
keys: keys(config.lobby.keys),
|
|
42
44
|
windows: [],
|
|
43
45
|
};
|
|
44
46
|
}
|
package/src/webui/api/tasks.ts
CHANGED
|
@@ -15,6 +15,37 @@ import { withAttachments } from "../uploads.ts";
|
|
|
15
15
|
import { fail } from "./index.ts";
|
|
16
16
|
import { taskSteps } from "./plan-facts.ts";
|
|
17
17
|
import type { TaskSection } from "../../lobby/task-rows.ts";
|
|
18
|
+
import { projectPhaseTiming } from "../../state/phase-timing.ts";
|
|
19
|
+
import type { DeliveryRequest } from "../../delivery/operations.ts";
|
|
20
|
+
|
|
21
|
+
function deliveryTask(body: { taskId: string }, ctx: ApiContext): Task {
|
|
22
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,199}$/.test(body.taskId)) fail(400, "bad_request", "invalid task ID");
|
|
23
|
+
const task = ctx.service.tasks().find((entry) => entry.id === body.taskId);
|
|
24
|
+
if (!task) fail(404, "not_found", "task not found in this project");
|
|
25
|
+
if (task.state !== "completed" || !task.delivery) fail(400, "bad_request", "this task has no completed-work delivery review");
|
|
26
|
+
return task;
|
|
27
|
+
}
|
|
28
|
+
export async function tasksDeliveryReview(body: { taskId: string }, ctx: ApiContext) {
|
|
29
|
+
deliveryTask(body, ctx);
|
|
30
|
+
if (!ctx.service.deliveryReview) fail(400, "bad_request", "delivery review is unavailable");
|
|
31
|
+
try { return { delivery: await ctx.service.deliveryReview(body.taskId) }; }
|
|
32
|
+
catch (error) { return fail(400, "bad_request", (error as Error).message); }
|
|
33
|
+
}
|
|
34
|
+
export async function tasksDeliver(body: { taskId: string } & DeliveryRequest, ctx: ApiContext) {
|
|
35
|
+
const task = deliveryTask(body, ctx);
|
|
36
|
+
if (task.delivery!.reviewId !== body.reviewId) fail(409, "conflict", "review changed; refresh before delivery");
|
|
37
|
+
if (body.action === "merge_main" && body.confirmMain !== true) fail(400, "bad_request", "confirm that direct merge updates and pushes main");
|
|
38
|
+
if (!ctx.service.deliveryDeliver) fail(400, "bad_request", "delivery is unavailable");
|
|
39
|
+
try { return { delivery: await ctx.service.deliveryDeliver(body.taskId, body) }; }
|
|
40
|
+
catch (error) { return fail(409, "conflict", (error as Error).message); }
|
|
41
|
+
}
|
|
42
|
+
export function tasksDeliveryDefer(body: { taskId: string; reviewId: string }, ctx: ApiContext) {
|
|
43
|
+
const task = deliveryTask(body, ctx);
|
|
44
|
+
if (task.delivery!.reviewId !== body.reviewId) fail(409, "conflict", "review changed; refresh before deferring");
|
|
45
|
+
if (!ctx.service.deliveryDefer) fail(400, "bad_request", "delivery review is unavailable");
|
|
46
|
+
try { return { delivery: ctx.service.deliveryDefer(body.taskId, body.reviewId) }; }
|
|
47
|
+
catch (error) { return fail(400, "bad_request", (error as Error).message); }
|
|
48
|
+
}
|
|
18
49
|
|
|
19
50
|
interface RowContext {
|
|
20
51
|
names: ReadonlyMap<string, string>;
|
|
@@ -62,6 +93,8 @@ function rowOf(task: Task, section: TaskSection, ctx: RowContext, sessionId: str
|
|
|
62
93
|
title: task.title,
|
|
63
94
|
section,
|
|
64
95
|
status: task.state,
|
|
96
|
+
timing: projectPhaseTiming(task, new Date(now).toISOString()),
|
|
97
|
+
...(task.delivery ? { delivery: { status: task.delivery.status, reviewId: task.delivery.reviewId } } : {}),
|
|
65
98
|
...(task.paused && section !== "recent" ? { paused: true } : {}),
|
|
66
99
|
check: checkOf(task),
|
|
67
100
|
...(progress ? { progress } : {}),
|
|
@@ -94,6 +127,8 @@ function buildRows(tasks: readonly Task[], plans: readonly PlannedTask[], sessio
|
|
|
94
127
|
title: task.title,
|
|
95
128
|
section: "archived",
|
|
96
129
|
status: task.state,
|
|
130
|
+
timing: projectPhaseTiming(task, new Date(now).toISOString()),
|
|
131
|
+
...(task.delivery ? { delivery: { status: task.delivery.status, reviewId: task.delivery.reviewId } } : {}),
|
|
97
132
|
check: checkOf(task),
|
|
98
133
|
age: ago(now - Date.parse(task.archivedAt ?? task.updatedAt)),
|
|
99
134
|
})),
|
|
@@ -125,6 +160,8 @@ export function tasksArchived(ctx: ApiContext): { rows: TaskRow[] } {
|
|
|
125
160
|
title: task.title,
|
|
126
161
|
section: "archived",
|
|
127
162
|
status: task.state,
|
|
163
|
+
timing: projectPhaseTiming(task),
|
|
164
|
+
...(task.delivery ? { delivery: { status: task.delivery.status, reviewId: task.delivery.reviewId } } : {}),
|
|
128
165
|
check: checkOf(task),
|
|
129
166
|
age: ago(Date.now() - Date.parse(task.archivedAt ?? task.updatedAt)),
|
|
130
167
|
}));
|
|
@@ -190,6 +227,19 @@ export function tasksMessage(body: { taskId: string; text: string; attachments?:
|
|
|
190
227
|
return { notice: ctx.service.sendToTask(body.taskId, withAttachments(body.text, body.attachments, body.taskId, ctx.service.projectRoot?.())) };
|
|
191
228
|
}
|
|
192
229
|
|
|
230
|
+
/** Select existing owner chat metadata only; never start, claim or switch a process. */
|
|
231
|
+
export function tasksOpen(body: { taskId: string }, ctx: ApiContext): { sessionId?: string; key?: string; notice?: string } {
|
|
232
|
+
const task = ctx.service.tasks().find((entry) => entry.id === body.taskId);
|
|
233
|
+
if (!task) fail(404, "not_found", `no task ${body.taskId} in this project`);
|
|
234
|
+
const sessionId = task.ownerSessionId;
|
|
235
|
+
if (!sessionId) return { notice: "This task has no owning session. No replacement session was created." };
|
|
236
|
+
if (sessionId === ctx.service.sessionId()) return { sessionId };
|
|
237
|
+
const background = ctx.service.sessions().find((entry) => entry.sessionId === sessionId && entry.alive);
|
|
238
|
+
if (background) return { sessionId, key: background.key };
|
|
239
|
+
if (ctx.service.liveSessions().some((entry) => entry.sessionId === sessionId)) return { sessionId };
|
|
240
|
+
return { notice: "The task's owning session has ended or is unavailable. No replacement session was created." };
|
|
241
|
+
}
|
|
242
|
+
|
|
193
243
|
/** How many recent runs the detail lists (the terminal's count). */
|
|
194
244
|
const DETAIL_RUNS = 6;
|
|
195
245
|
|
|
@@ -206,6 +256,8 @@ export function tasksGet(body: { taskId: string }, ctx: ApiContext): TaskDetail
|
|
|
206
256
|
const request = taskRequest(found);
|
|
207
257
|
const now = Date.now();
|
|
208
258
|
return {
|
|
259
|
+
timing: projectPhaseTiming(found, new Date(now).toISOString()),
|
|
260
|
+
...(found.delivery ? { delivery: found.delivery } : {}),
|
|
209
261
|
...(request !== found.title ? { request } : {}),
|
|
210
262
|
...(!found.plan && found.proposal ? { proposal: found.proposal } : {}),
|
|
211
263
|
...(found.plan ? { plan: found.plan } : {}),
|
package/src/webui/protocol.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* The page imports this file with `import type` only, so nothing from the
|
|
4
4
|
* server is ever bundled into it.
|
|
5
5
|
*/
|
|
6
|
+
import type { Delivery, DeliveryAction } from "../delivery/types.ts";
|
|
7
|
+
import type { TimingProjection } from "../state/phase-timing.ts";
|
|
6
8
|
import type { LobbyTopic } from "../lobby/topics.ts";
|
|
7
9
|
import type { WebPrompt } from "../lobby/prompt-hub.ts";
|
|
8
10
|
import type { DialogAnswer, SessionDialog } from "../lobby/sessions.ts";
|
|
@@ -338,6 +340,8 @@ export interface ExcalidrawCheck {
|
|
|
338
340
|
|
|
339
341
|
/** `tasks.get`: one task read whole, in the terminal's wording. */
|
|
340
342
|
export interface TaskDetail {
|
|
343
|
+
timing?: TimingProjection;
|
|
344
|
+
delivery?: Delivery;
|
|
341
345
|
/** The full request, when it says more than the title. */
|
|
342
346
|
request?: string;
|
|
343
347
|
/** The oracle's proposal, while the task has no approved plan yet. */
|
|
@@ -519,6 +523,10 @@ export interface Api {
|
|
|
519
523
|
"tasks.auto": { request: { taskId: string; on: boolean }; result: { notice: string; on: boolean } };
|
|
520
524
|
"tasks.message": { request: { taskId: string; text: string; attachments?: string[] }; result: { notice: string } };
|
|
521
525
|
"tasks.get": { request: { taskId: string }; result: TaskDetail };
|
|
526
|
+
"tasks.open": { request: { taskId: string }; result: { sessionId?: string; key?: string; notice?: string } };
|
|
527
|
+
"tasks.deliveryReview": { request: { taskId: string }; result: { delivery: Delivery } };
|
|
528
|
+
"tasks.deliveryDefer": { request: { taskId: string; reviewId: string }; result: { delivery: Delivery } };
|
|
529
|
+
"tasks.deliver": { request: { taskId: string; reviewId: string; action: DeliveryAction; confirmMain?: boolean }; result: { delivery: Delivery } };
|
|
522
530
|
"plans.get": { request: { planId: string }; result: PlanDetail };
|
|
523
531
|
"plans.start": { request: { planId: string; where: "here" | "session"; auto?: boolean }; result: { notice: string; key?: string } };
|
|
524
532
|
"plans.discard": { request: { planId: string }; result: { notice: string } };
|