@a-t-h-i/bot-lobby 0.6.3 → 0.6.4
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 +102 -11
- package/package.json +1 -4
- package/prompts/master.md +47 -1
- package/prompts/researcher.md +8 -2
- package/prompts/reviewer.md +42 -0
- package/prompts/worker.md +7 -0
- package/src/ask/dialog.ts +167 -0
- package/src/ask/image.ts +202 -0
- package/src/ask/png.ts +179 -0
- package/src/ask/relay.ts +89 -0
- package/src/ask/state.ts +160 -0
- package/src/ask/tool.ts +126 -0
- package/src/ask/types.ts +55 -0
- package/src/ask/view.ts +159 -0
- package/src/execution/agent-runner.ts +103 -11
- package/src/execution/git.ts +111 -14
- package/src/execution/pi-runner.ts +164 -11
- package/src/index.ts +6 -0
- package/src/lobby/ask.ts +10 -120
- package/src/lobby/layout.ts +27 -8
- package/src/lobby/markdown.ts +36 -6
- package/src/lobby/planner.ts +1 -1
- package/src/lobby/quickfix.ts +21 -0
- package/src/lobby/runtime.ts +14 -36
- package/src/lobby/tabs/home.ts +112 -48
- package/src/lobby/tabs/issues.ts +4 -3
- package/src/lobby/tabs/plan.ts +4 -4
- package/src/lobby/tabs/quickfix.ts +7 -1
- package/src/lobby/tabs/tasks.ts +14 -4
- package/src/lobby/theme.ts +30 -0
- package/src/lobby/view.ts +27 -5
- package/src/master/decisions.ts +1 -1
- package/src/master/master.ts +41 -4
- package/src/master/research.ts +5 -2
- package/src/pi/commands.ts +94 -10
- package/src/pi/events.ts +47 -10
- package/src/pi/quiet.ts +22 -4
- package/src/pi/start-task.ts +8 -2
- package/src/pi/tools.ts +26 -8
- package/src/pi/ui.ts +6 -1
- package/src/pi/zen-metrics.ts +13 -3
- package/src/pi/zen.ts +16 -9
- package/src/roles/reviewer.ts +23 -4
- package/src/roles/worker.ts +18 -0
- package/src/schemas/configuration.ts +4 -0
- package/src/schemas/findings.ts +14 -0
- package/src/schemas/task.ts +21 -0
- package/src/state/budget.ts +274 -0
- package/src/state/changes.ts +231 -0
- package/src/text.ts +28 -2
- package/src/web/extract.ts +332 -0
- package/src/web/fetch.ts +232 -0
- package/src/web/html.ts +183 -0
- package/src/web/read.ts +113 -0
- package/src/web/search.ts +202 -0
- package/src/web/tools.ts +279 -0
- package/src/workflow/workflow.ts +592 -42
package/src/pi/events.ts
CHANGED
|
@@ -9,20 +9,37 @@ import { describeTask } from "../workflow/workflow.ts";
|
|
|
9
9
|
import { truncate } from "../text.ts";
|
|
10
10
|
import { applyStatus, clearStatus, isMinimized, setMinimized, setOracleActivity } from "./ui.ts";
|
|
11
11
|
import { ORACLE_THINKING, oracleActivityWord } from "./activity.ts";
|
|
12
|
-
import { isSubagentProcess,
|
|
12
|
+
import { isSubagentProcess, webToolsFor } from "./quiet.ts";
|
|
13
13
|
import { registerQuietTools } from "./tool-renderers.ts";
|
|
14
14
|
import { taskRequest, type Task, type TaskState } from "../schemas/task.ts";
|
|
15
15
|
import { pendingComments, readPlanComments, type PlanComment } from "../state/comments.ts";
|
|
16
16
|
import { isAutoMode } from "../state/auto.ts";
|
|
17
17
|
import { triageContext } from "../classifier/triage.ts";
|
|
18
18
|
import { previousTaskNote } from "./fresh-context.ts";
|
|
19
|
+
import { budgetLine, budgetState, pauseClocks, readBudget, resumeClocks, startClock, stopClocks } from "../state/budget.ts";
|
|
20
|
+
|
|
21
|
+
/** Tools that wait on the user: the task's clock waits with them. */
|
|
22
|
+
const ASKING_TOOLS: ReadonlySet<string> = new Set(["ask_user_question"]);
|
|
19
23
|
|
|
20
24
|
/** States in which the triage still helps the Master shape the task; once it is planned, the hints are noise. */
|
|
21
25
|
const SHAPING_STATES: ReadonlySet<TaskState> = new Set(["created", "clarifying", "scouting", "synthesizing", "awaiting_approval"]);
|
|
22
26
|
|
|
23
|
-
/** The Master's workflow context: the task's state,
|
|
24
|
-
export function masterWorkflowContext(task: Task): string {
|
|
25
|
-
return [describeTask(task), SHAPING_STATES.has(task.state) ? triageContext(task.triage) : ""].filter(Boolean).join("\n\n");
|
|
27
|
+
/** The Master's workflow context: the task's state, where its time budget stands, and the classifier's triage while the task is being shaped. */
|
|
28
|
+
export function masterWorkflowContext(task: Task, time = ""): string {
|
|
29
|
+
return [describeTask(task), time, SHAPING_STATES.has(task.state) ? triageContext(task.triage) : ""].filter(Boolean).join("\n\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The oracle's view of the task's time budget: where it stands and how to spend it; "" without one. */
|
|
33
|
+
export function budgetContext(root: string, configDir: string, task: Task): string {
|
|
34
|
+
const budget = readBudget(root, configDir, task.id);
|
|
35
|
+
if (!budget) return "";
|
|
36
|
+
const state = budgetState(task.id, budget, task.qaVerdict === "pass");
|
|
37
|
+
const running = budget.allotments.filter((entry) => !entry.endedAt).map((entry) => `${entry.who} (${entry.minutes}m)`);
|
|
38
|
+
return [
|
|
39
|
+
budgetLine(budget, state),
|
|
40
|
+
running.length > 0 ? `Running now: ${running.join(", ")}.` : "",
|
|
41
|
+
"Divide what is left by scope: pass `minutes` on each implement (per assignment in a parallel batch), keeping the QA gate's reserve. Every agent is told its minutes; one that runs out reports where it left off and the user decides on more.",
|
|
42
|
+
].filter(Boolean).join(" ");
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
export function masterTaskContext(task: Task, comments: readonly PlanComment[] = [], auto = false): string {
|
|
@@ -47,12 +64,11 @@ export function masterTaskContext(task: Task, comments: readonly PlanComment[] =
|
|
|
47
64
|
* section rather than replacing the whole prompt.
|
|
48
65
|
*/
|
|
49
66
|
export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
67
|
+
// Web tools the oracle put away for the task in hand, given back when it ends.
|
|
68
|
+
let hiddenWeb: string[] = [];
|
|
50
69
|
pi.on("session_start", (_event, ctx) => {
|
|
51
70
|
// Master-only: subagents keep their own --tools allowlist (see quiet.ts).
|
|
52
|
-
if (!isSubagentProcess())
|
|
53
|
-
registerQuietTools(pi);
|
|
54
|
-
pi.setActiveTools(visibleTools(pi.getActiveTools()));
|
|
55
|
-
}
|
|
71
|
+
if (!isSubagentProcess()) registerQuietTools(pi);
|
|
56
72
|
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
57
73
|
setMinimized(false);
|
|
58
74
|
applyStatus(ctx, root, configDir);
|
|
@@ -65,25 +81,38 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
65
81
|
// Parallel tool calls: the newest still-running call keeps the word.
|
|
66
82
|
const inFlight = new Map<string, string>();
|
|
67
83
|
const showOracle = () => setOracleActivity([...inFlight.values()].at(-1) ?? ORACLE_THINKING);
|
|
68
|
-
|
|
84
|
+
// A task's time budget counts while the oracle works on it, not while it waits on the user.
|
|
85
|
+
const asking = new Set<string>();
|
|
86
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
69
87
|
if (isSubagentProcess()) return;
|
|
70
88
|
inFlight.clear();
|
|
71
89
|
showOracle();
|
|
90
|
+
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
91
|
+
const task = activeTask(root, configDir, ctx.sessionManager.getSessionId());
|
|
92
|
+
if (task) startClock(root, configDir, task.id);
|
|
72
93
|
});
|
|
73
94
|
pi.on("tool_execution_start", (event) => {
|
|
74
95
|
if (isSubagentProcess()) return;
|
|
75
96
|
inFlight.set(event.toolCallId, oracleActivityWord(event.toolName, event.args));
|
|
76
97
|
showOracle();
|
|
98
|
+
if (ASKING_TOOLS.has(event.toolName) && !asking.has(event.toolCallId)) {
|
|
99
|
+
asking.add(event.toolCallId);
|
|
100
|
+
pauseClocks();
|
|
101
|
+
}
|
|
77
102
|
});
|
|
78
103
|
pi.on("tool_execution_end", (event) => {
|
|
79
104
|
if (isSubagentProcess()) return;
|
|
80
105
|
inFlight.delete(event.toolCallId);
|
|
81
106
|
showOracle();
|
|
107
|
+
if (asking.delete(event.toolCallId)) resumeClocks();
|
|
82
108
|
});
|
|
83
109
|
pi.on("agent_end", () => {
|
|
84
110
|
if (isSubagentProcess()) return;
|
|
85
111
|
inFlight.clear();
|
|
86
112
|
setOracleActivity(undefined);
|
|
113
|
+
for (const _call of asking) resumeClocks();
|
|
114
|
+
asking.clear();
|
|
115
|
+
stopClocks();
|
|
87
116
|
});
|
|
88
117
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
89
118
|
cancelAllRuns();
|
|
@@ -95,6 +124,14 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
95
124
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
96
125
|
const hidden = isSubagentProcess() || isMinimized();
|
|
97
126
|
const task = hidden ? undefined : activeTask(root, configDir, sessionId);
|
|
127
|
+
if (!isSubagentProcess()) {
|
|
128
|
+
// The researcher is the task's only web path; without a task pi keeps them.
|
|
129
|
+
const tools = webToolsFor(pi.getActiveTools(), Boolean(task), hiddenWeb);
|
|
130
|
+
if (tools) {
|
|
131
|
+
hiddenWeb = tools.hidden;
|
|
132
|
+
pi.setActiveTools(tools.active);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
98
135
|
if (!task) {
|
|
99
136
|
// After a task ended and cleared the context, a line on where its record is, in case the user refers back to it.
|
|
100
137
|
const previous = hidden ? undefined : previousTaskNote(ctx, configDir);
|
|
@@ -110,7 +147,7 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
110
147
|
standards: selected.standards,
|
|
111
148
|
knowledge: selected.knowledge,
|
|
112
149
|
decisions: selected.decisions,
|
|
113
|
-
workflowContext: masterWorkflowContext(task),
|
|
150
|
+
workflowContext: masterWorkflowContext(task, budgetContext(root, configDir, task)),
|
|
114
151
|
instructions: loadConfig().master.instructions,
|
|
115
152
|
});
|
|
116
153
|
});
|
package/src/pi/quiet.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Quiet mode for the master transcript: built-in tool rows render zero lines
|
|
3
|
-
* by default and `alt+t` reveals a compact one-line summary per call.
|
|
4
|
-
* tools are removed from the master's active set so the
|
|
5
|
-
* running in its own process, stays the only web
|
|
3
|
+
* by default and `alt+t` reveals a compact one-line summary per call. While a
|
|
4
|
+
* task runs, the web tools are removed from the master's active set so the
|
|
5
|
+
* researcher subagent, running in its own process, stays the task's only web
|
|
6
|
+
* path; without a task, pi has them like any other tool.
|
|
6
7
|
*
|
|
7
8
|
* bot-lobby's extension also loads inside the subagent processes it spawns
|
|
8
9
|
* (see `spawnPiProcess`). Those children receive their tools from an explicit
|
|
@@ -10,7 +11,7 @@
|
|
|
10
11
|
* never run there or research silently degrades to repository-only.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
+
/** bot-lobby's web tools (src/web/tools.ts): the researcher's, and pi's outside a task, never the oracle's. */
|
|
14
15
|
export const WEB_TOOL_NAMES: readonly string[] = [
|
|
15
16
|
"web_search",
|
|
16
17
|
"fetch_content",
|
|
@@ -40,6 +41,23 @@ export function visibleTools(active: string[]): string[] {
|
|
|
40
41
|
return active.filter((name) => !WEB_TOOL_NAMES.includes(name));
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The master's active tools for a turn: without the web tools while a task
|
|
46
|
+
* is active, and with those it took away given back once none is. Only tools
|
|
47
|
+
* it hid itself come back, so a tool the user turned off stays off. Returns
|
|
48
|
+
* undefined when nothing changes.
|
|
49
|
+
*/
|
|
50
|
+
export function webToolsFor(active: string[], taskActive: boolean, hidden: readonly string[]): { active: string[]; hidden: string[] } | undefined {
|
|
51
|
+
if (taskActive) {
|
|
52
|
+
const taken = active.filter((name) => WEB_TOOL_NAMES.includes(name));
|
|
53
|
+
if (taken.length === 0) return undefined;
|
|
54
|
+
return { active: visibleTools(active), hidden: [...new Set([...hidden, ...taken])] };
|
|
55
|
+
}
|
|
56
|
+
const back = hidden.filter((name) => !active.includes(name));
|
|
57
|
+
if (hidden.length === 0) return undefined;
|
|
58
|
+
return { active: [...active, ...back], hidden: [] };
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
/** True inside a bot-lobby subagent process (marked in `spawnPiProcess`). */
|
|
44
62
|
export function isSubagentProcess(): boolean {
|
|
45
63
|
return process.env.BOT_LOBBY_SUBAGENT === "1";
|
package/src/pi/start-task.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { setAutoMode } from "../state/auto.ts";
|
|
|
16
16
|
import { loadPlannedTask, markPlannedTaskStarted, plannedTaskRequest } from "../state/backlog.ts";
|
|
17
17
|
import { triageFor } from "../classifier/instance.ts";
|
|
18
18
|
import { freshContextOn, markContext } from "./fresh-context.ts";
|
|
19
|
+
import { setBudget } from "../state/budget.ts";
|
|
19
20
|
|
|
20
21
|
function uniqueTaskId(root: string, configDir: string, request: string): string {
|
|
21
22
|
const base = nextTaskId(request);
|
|
@@ -25,12 +26,13 @@ function uniqueTaskId(root: string, configDir: string, request: string): string
|
|
|
25
26
|
return id;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
export function kickoff(task: Task): string {
|
|
29
|
+
export function kickoff(task: Task, budgetMinutes = 0): string {
|
|
29
30
|
return [
|
|
30
31
|
`A bot-lobby task is active: ${task.id}`,
|
|
31
32
|
`Title: ${task.title}`,
|
|
32
33
|
`Request: ${taskRequest(task)}`,
|
|
33
34
|
`State: ${task.state}`,
|
|
35
|
+
...(budgetMinutes > 0 ? [`Time budget: ${budgetMinutes} minutes of work, for you and every agent. Size the plan to fit it and divide it by scope (see Time budget in your prompt).`] : []),
|
|
34
36
|
"",
|
|
35
37
|
"Drive it with the orchestrate tool:",
|
|
36
38
|
"1. clarify if the request is genuinely ambiguous,",
|
|
@@ -52,6 +54,8 @@ export interface StartOptions {
|
|
|
52
54
|
auto?: boolean;
|
|
53
55
|
/** The task's short title (a planned task's own title); derived from the request otherwise. */
|
|
54
56
|
title?: string;
|
|
57
|
+
/** Minutes of work time the task gets; `workflow.taskBudgetMinutes` when absent. */
|
|
58
|
+
budget?: number;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
export async function startTask(pi: ExtensionAPI, ctx: ExtensionContext, configDir: string, request: string, options: StartOptions = {}): Promise<Task | undefined> {
|
|
@@ -71,6 +75,8 @@ export async function startTask(pi: ExtensionAPI, ctx: ExtensionContext, configD
|
|
|
71
75
|
const triage = await triageFor({ cwd: ctx.cwd, root, configDir }, request);
|
|
72
76
|
if (triage) task.triage = triage;
|
|
73
77
|
saveTask(root, configDir, task);
|
|
78
|
+
const budgetMinutes = options.budget ?? loadConfig().workflow.taskBudgetMinutes;
|
|
79
|
+
if (budgetMinutes > 0) setBudget(root, configDir, task.id, budgetMinutes);
|
|
74
80
|
if (options.auto) setAutoMode(root, configDir, task.id, true, sessionId);
|
|
75
81
|
// A session that starts a task is named after it, so /resume and the lobby list it by name.
|
|
76
82
|
if (!pi.getSessionName()) pi.setSessionName(task.title);
|
|
@@ -80,7 +86,7 @@ export async function startTask(pi: ExtensionAPI, ctx: ExtensionContext, configD
|
|
|
80
86
|
// The oracle takes the task on with a clean context: nothing said before the kickoff is sent to its model.
|
|
81
87
|
if (freshContextOn()) markContext(pi, { kind: "start", taskId: task.id, at: Date.now() });
|
|
82
88
|
// A kickoff while pi is still busy (another turn) queues behind it instead of throwing.
|
|
83
|
-
pi.sendUserMessage(kickoff(task), ctx.isIdle() ? undefined : { deliverAs: "followUp" });
|
|
89
|
+
pi.sendUserMessage(kickoff(task, budgetMinutes), ctx.isIdle() ? undefined : { deliverAs: "followUp" });
|
|
84
90
|
return task;
|
|
85
91
|
}
|
|
86
92
|
|
package/src/pi/tools.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
-
import type
|
|
3
|
-
import { Container, Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { getMarkdownTheme, type AgentToolUpdateCallback, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Container, Markdown, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import type { AgentRun } from "../schemas/findings.ts";
|
|
6
6
|
import type { ProcessRunner } from "../execution/pi-runner.ts";
|
|
@@ -8,6 +8,10 @@ import { detectProjectRoot, loadConfig } from "../state/project.ts";
|
|
|
8
8
|
import { classifier, effortFor, hintsFor, triageFor } from "../classifier/instance.ts";
|
|
9
9
|
import { truncate } from "../text.ts";
|
|
10
10
|
import { applyStatus, reportRuns, summarizeRun } from "./ui.ts";
|
|
11
|
+
import { whileAsking } from "../state/budget.ts";
|
|
12
|
+
import { askUser } from "../ask/dialog.ts";
|
|
13
|
+
import type { AskQuestion } from "../ask/types.ts";
|
|
14
|
+
import { unescapeBreaks } from "../lobby/markdown.ts";
|
|
11
15
|
import { isQuiet } from "./quiet.ts";
|
|
12
16
|
import { checkThinking, createProfileResolver, modelRef, type ModelLookup } from "./model-support.ts";
|
|
13
17
|
import { agentName, describeRun } from "./run-summary.ts";
|
|
@@ -36,16 +40,18 @@ const OrchestrateSchema = Type.Object({
|
|
|
36
40
|
Type.Object({
|
|
37
41
|
domain: Type.String({ description: "designer, backend, or qa" }),
|
|
38
42
|
task: Type.String({ description: "the concrete step(s) for that domain's worker" }),
|
|
43
|
+
minutes: Type.Optional(Type.Number({ description: "under a time budget: minutes for this worker, by its scope" })),
|
|
39
44
|
}),
|
|
40
45
|
{ description: "implement: run several domains in parallel (distinct domains); workers share files through the file desk" },
|
|
41
46
|
),
|
|
42
47
|
),
|
|
48
|
+
minutes: Type.Optional(Type.Number({ description: "implement (under a time budget): minutes for this step, by its scope; budget: more minutes to ask the user for" })),
|
|
43
49
|
approvalId: Type.Optional(Type.String({ description: "resolve_approval: the approval id from a worker result" })),
|
|
44
50
|
decision: Type.Optional(
|
|
45
51
|
StringEnum(["approved", "rejected"] as const, { description: "resolve_approval: approve or reject the request" }),
|
|
46
52
|
),
|
|
47
53
|
note: Type.Optional(Type.String({ description: "resolve_approval: rationale, or what to do instead when rejected" })),
|
|
48
|
-
reason: Type.Optional(Type.String({ description: "block: why the task cannot continue" })),
|
|
54
|
+
reason: Type.Optional(Type.String({ description: "block: why the task cannot continue; budget: why the task needs more time" })),
|
|
49
55
|
text: Type.Optional(Type.String({ description: "decide/complete/knowledge: the decision, completion summary, or knowledge text" })),
|
|
50
56
|
file: Type.Optional(Type.String({ description: "compact: the knowledge file to rewrite, e.g. knowledge.md" })),
|
|
51
57
|
kind: Type.Optional(
|
|
@@ -64,7 +70,8 @@ const DESCRIPTION = [
|
|
|
64
70
|
"review), knowledge (record approved knowledge or a decision),",
|
|
65
71
|
"compact (replace a knowledge file with a rewritten version, archiving the old one),",
|
|
66
72
|
"resolve_approval (approve or reject a request), complete (declare the task done after the gates",
|
|
67
|
-
"pass), block/resume (escalate or continue),
|
|
73
|
+
"pass), block/resume (escalate or continue), budget (under a time budget: where it stands, or ask the",
|
|
74
|
+
"user for more minutes with a reason), status, cancel.",
|
|
68
75
|
"The engine validates every step against the task state machine, so a rejected action means the workflow is not at that step yet.",
|
|
69
76
|
].join(" ");
|
|
70
77
|
|
|
@@ -109,8 +116,11 @@ export function workflowDeps(
|
|
|
109
116
|
classifier: classifier(),
|
|
110
117
|
triage: (request, triageSignal) => triageFor({ cwd: ctx.cwd, root, configDir }, request, triageSignal),
|
|
111
118
|
effort: effortFor((model, thinking) => checkThinking(modelLookup(ctx)(model), thinking).level),
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
// Time spent waiting on the user is not the task's: its budget clock waits too.
|
|
120
|
+
ask: async (question) => (hasUI ? whileAsking(() => ctx.ui.input(question)) : undefined),
|
|
121
|
+
choose: async (title, options) => (hasUI ? whileAsking(() => ctx.ui.select(title, options)) : undefined),
|
|
122
|
+
// An agent's own questions (the designer's), relayed as the questionnaire.
|
|
123
|
+
...(hasUI ? { askQuestions: (questions: AskQuestion[], from: string, askSignal?: AbortSignal) => whileAsking(() => askUser(questions, ctx, askSignal, from)) } : {}),
|
|
114
124
|
notify: (message, level = "info") => {
|
|
115
125
|
if (hasUI) ctx.ui.notify(message, level);
|
|
116
126
|
},
|
|
@@ -135,6 +145,14 @@ function runReporter(
|
|
|
135
145
|
};
|
|
136
146
|
}
|
|
137
147
|
|
|
148
|
+
/** A header line over Markdown, rendered as pi renders its own messages (a proposal's bullets, a report's code spans). */
|
|
149
|
+
function headedMarkdown(header: string, body: string, color: (text: string) => string): Container {
|
|
150
|
+
const box = new Container();
|
|
151
|
+
box.addChild(new Text(header, 0, 0));
|
|
152
|
+
if (body.trim()) box.addChild(new Markdown(unescapeBreaks(body), 0, 0, getMarkdownTheme(), { color }));
|
|
153
|
+
return box;
|
|
154
|
+
}
|
|
155
|
+
|
|
138
156
|
/** TUI-only transcript entries; these never enter the model's context. */
|
|
139
157
|
function registerBotLobbyEntries(pi: ExtensionAPI): void {
|
|
140
158
|
pi.registerEntryRenderer("bot-lobby", (entry, { expanded }, theme) => {
|
|
@@ -145,7 +163,7 @@ function registerBotLobbyEntries(pi: ExtensionAPI): void {
|
|
|
145
163
|
}
|
|
146
164
|
const header = `bot-lobby ${data?.taskId ?? ""} — ${data?.kind ?? "note"}`.trim();
|
|
147
165
|
const body = data?.text ?? "";
|
|
148
|
-
return
|
|
166
|
+
return headedMarkdown(theme.fg("accent", theme.bold(header)), expanded ? body : truncate(body, 600), (text) => theme.fg("toolOutput", text));
|
|
149
167
|
});
|
|
150
168
|
}
|
|
151
169
|
|
|
@@ -200,7 +218,7 @@ export function registerOrchestrateTool(pi: ExtensionAPI, configDir: string, run
|
|
|
200
218
|
const body = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
201
219
|
const icon = details?.ok ? theme.fg("success", "✓") : theme.fg("warning", "!");
|
|
202
220
|
const header = `${icon} ${theme.fg("toolTitle", theme.bold("orchestrate"))} ${theme.fg("accent", details?.taskId ?? "")} ${theme.fg("muted", `→ ${details?.state ?? "?"}`)}`;
|
|
203
|
-
return
|
|
221
|
+
return expanded && body ? headedMarkdown(header, truncate(body, 2000), (text) => theme.fg("dim", text)) : new Text(header, 0, 0);
|
|
204
222
|
},
|
|
205
223
|
});
|
|
206
224
|
}
|
package/src/pi/ui.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { slotSituations } from "./zen-metrics.ts";
|
|
|
26
26
|
import type { Situation } from "./kaomoji.ts";
|
|
27
27
|
import { isQuiet, isSubagentProcess, toggleQuiet } from "./quiet.ts";
|
|
28
28
|
import { panelLines, type ExpressionFrames, type OracleMotion } from "./zen.ts";
|
|
29
|
+
import { budgetClock } from "../state/budget.ts";
|
|
29
30
|
|
|
30
31
|
export const STATUS_KEY = "bot-lobby";
|
|
31
32
|
|
|
@@ -52,6 +53,8 @@ let zenOn = false;
|
|
|
52
53
|
* keys the widget's render cache.
|
|
53
54
|
*/
|
|
54
55
|
let zenState: { task: Task | undefined; live: AgentRun[]; runs: AgentRun[] } = { task: undefined, live: [], runs: [] };
|
|
56
|
+
/** Where the widget's task lives, so its time budget can be read. */
|
|
57
|
+
let zenPaths: { root: string; configDir: string } | undefined;
|
|
55
58
|
let zenVersion = 0;
|
|
56
59
|
|
|
57
60
|
/** Latest master tool activity; the oracle's speech bubble shows it. */
|
|
@@ -255,7 +258,8 @@ export class ZenScene {
|
|
|
255
258
|
const motion = this.motion(now);
|
|
256
259
|
const key = `${width}|${rows}|${this.tick}|${frameKey}|${motion.phase}|${motion.talk}|${zenVersion}|${Math.floor(now / 1000)}|${quiet}|${still}`;
|
|
257
260
|
if (this.cache && this.cache.key === key && this.cache.theme === theme) return this.cache.lines;
|
|
258
|
-
const
|
|
261
|
+
const time = zenState.task && zenPaths ? budgetClock(zenPaths.root, zenPaths.configDir, zenState.task.id, now) : undefined;
|
|
262
|
+
const opts = { width, rows, tick: this.tick, theme, expressions, variants, oracleActivity, oracleMotion: motion, still, ...(time ? { time } : {}) };
|
|
259
263
|
const lines = panelLines(zenState.task, zenState.runs, now, quiet, opts).map((line) => clip(line, width));
|
|
260
264
|
this.cache = { key, theme, lines };
|
|
261
265
|
return lines;
|
|
@@ -356,6 +360,7 @@ function leaveZen(ctx: ExtensionContext): void {
|
|
|
356
360
|
export function applyStatus(ctx: ExtensionContext, root: string, configDir: string, runs: AgentRun[] = []): void {
|
|
357
361
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
358
362
|
const task = isSubagentProcess() || minimized ? undefined : activeTask(root, configDir, sessionId);
|
|
363
|
+
zenPaths = { root, configDir };
|
|
359
364
|
const sameTask = zenState.task?.id === task?.id;
|
|
360
365
|
setZenState(task, mergeRuns(sameTask ? zenState.live : [], slim(runs)));
|
|
361
366
|
ctx.ui.setStatus(STATUS_KEY, statusText(task, minimized));
|
package/src/pi/zen-metrics.ts
CHANGED
|
@@ -11,6 +11,13 @@ import { SLOT_IDS, SLOT_LABELS, type SlotId, type SlotState } from "./mascot-art
|
|
|
11
11
|
import { formatDuration, planChecklist } from "./zen.ts";
|
|
12
12
|
import { QUIET_MS, quietFor, slotOf } from "./run-summary.ts";
|
|
13
13
|
import type { Situation } from "./kaomoji.ts";
|
|
14
|
+
import { formatMinutes } from "../state/budget.ts";
|
|
15
|
+
|
|
16
|
+
/** A task's time budget as the scene shows it: work time spent of the whole. */
|
|
17
|
+
export interface SceneTime {
|
|
18
|
+
usedMs: number;
|
|
19
|
+
totalMs: number;
|
|
20
|
+
}
|
|
14
21
|
|
|
15
22
|
export type { SlotId, SlotState };
|
|
16
23
|
|
|
@@ -70,7 +77,10 @@ function latestRunFor(runs: AgentRun[], id: SlotId): AgentRun | undefined {
|
|
|
70
77
|
function runElapsedLabel(run: AgentRun | undefined, now: number): string {
|
|
71
78
|
if (!run) return "—";
|
|
72
79
|
const end = run.finishedAt ? Date.parse(run.finishedAt) : now;
|
|
73
|
-
|
|
80
|
+
const elapsed = end - Date.parse(run.startedAt);
|
|
81
|
+
// Under a time budget: minutes spent of the minutes given (`12/30m`).
|
|
82
|
+
if (run.allotMs) return `${Math.max(0, Math.floor(elapsed / 60_000))}/${Math.round((run.allotMs + (run.extendedMs ?? 0)) / 60_000)}m`;
|
|
83
|
+
return formatDuration(elapsed);
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
function slotView(id: SlotId, runs: AgentRun[], now: number): SlotView {
|
|
@@ -89,7 +99,7 @@ function slotView(id: SlotId, runs: AgentRun[], now: number): SlotView {
|
|
|
89
99
|
};
|
|
90
100
|
}
|
|
91
101
|
|
|
92
|
-
export function sceneMetrics(task: Task, runs: AgentRun[], now: number): SceneMetrics {
|
|
102
|
+
export function sceneMetrics(task: Task, runs: AgentRun[], now: number, time?: SceneTime): SceneMetrics {
|
|
93
103
|
const checklist = planChecklist(task.plan ?? "", runs);
|
|
94
104
|
const total = checklist.length;
|
|
95
105
|
const done = checklist.filter((step) => step.status === "done").length;
|
|
@@ -98,7 +108,7 @@ export function sceneMetrics(task: Task, runs: AgentRun[], now: number): SceneMe
|
|
|
98
108
|
const elapsed = Number.isFinite(created) ? Math.max(0, now - created) : 0;
|
|
99
109
|
return {
|
|
100
110
|
...summary,
|
|
101
|
-
elapsedLabel: formatDuration(elapsed),
|
|
111
|
+
elapsedLabel: time ? `${formatMinutes(time.usedMs)} of ${formatMinutes(time.totalMs)}` : formatDuration(elapsed),
|
|
102
112
|
slots: SLOT_IDS.map((id) => slotView(id, runs, now)),
|
|
103
113
|
};
|
|
104
114
|
}
|
package/src/pi/zen.ts
CHANGED
|
@@ -37,7 +37,8 @@ import {
|
|
|
37
37
|
type LargeSlot,
|
|
38
38
|
type LargeTaskRow,
|
|
39
39
|
} from "./zen-large.ts";
|
|
40
|
-
import { runStatus, sceneMetrics, slotSituation, type SceneMetrics, type SlotView } from "./zen-metrics.ts";
|
|
40
|
+
import { runStatus, sceneMetrics, slotSituation, type SceneMetrics, type SceneTime, type SlotView } from "./zen-metrics.ts";
|
|
41
|
+
import { formatMinutes } from "../state/budget.ts";
|
|
41
42
|
import { feedLine, QUIET_MS, quietFor } from "./run-summary.ts";
|
|
42
43
|
import { shortDuration } from "../text.ts";
|
|
43
44
|
|
|
@@ -378,18 +379,22 @@ function checklistRow(step: PlanStep, ordinal: number): string {
|
|
|
378
379
|
return ` ${icon} ${ordinal}. ${step.text}`;
|
|
379
380
|
}
|
|
380
381
|
|
|
381
|
-
function headerLine(task: Task, now: number, quiet: boolean): string {
|
|
382
|
+
function headerLine(task: Task, now: number, quiet: boolean, time?: SceneTime): string {
|
|
382
383
|
const paused = task.paused ? " (paused)" : "";
|
|
383
|
-
const elapsed = formatDuration(now - Date.parse(task.createdAt));
|
|
384
|
+
const elapsed = time ? `${formatMinutes(time.usedMs)} of ${formatMinutes(time.totalMs)}` : formatDuration(now - Date.parse(task.createdAt));
|
|
384
385
|
const mode = quiet ? "tools hidden (alt+t)" : "tools shown";
|
|
385
386
|
return `bot-lobby ${task.id} · ${task.state}${paused} ⏱ ${elapsed} · ${mode}`;
|
|
386
387
|
}
|
|
387
388
|
|
|
388
|
-
/** The pending approval or blocker the user must resolve, with its severity. */
|
|
389
|
-
function taskAlert(task: Task): { text: string; kind: "warning" | "error" } | undefined {
|
|
389
|
+
/** The pending approval or blocker the user must resolve, with its severity; then a spent time budget. */
|
|
390
|
+
function taskAlert(task: Task, time?: SceneTime): { text: string; kind: "warning" | "error" } | undefined {
|
|
390
391
|
const pending = task.approvals.filter((approval) => approval.status === "pending");
|
|
391
392
|
if (pending.length > 0) return { text: `approvals pending: ${pending.map((approval) => approval.id).join(", ")}`, kind: "warning" };
|
|
392
393
|
if (task.blockers.length > 0) return { text: `blocked: ${truncate(task.blockers[0]!.reason, 60)}`, kind: "error" };
|
|
394
|
+
if (time && time.usedMs >= time.totalMs * 0.9) {
|
|
395
|
+
const spent = time.usedMs >= time.totalMs;
|
|
396
|
+
return { text: `time budget ${spent ? "spent" : "nearly spent"}: ${formatMinutes(time.usedMs)} of ${formatMinutes(time.totalMs)}`, kind: "warning" };
|
|
397
|
+
}
|
|
393
398
|
return undefined;
|
|
394
399
|
}
|
|
395
400
|
|
|
@@ -518,10 +523,10 @@ function compactPanel(
|
|
|
518
523
|
opts: PanelOptions,
|
|
519
524
|
width: number,
|
|
520
525
|
): string[] {
|
|
521
|
-
const metrics = sceneMetrics(task, runs, now);
|
|
526
|
+
const metrics = sceneMetrics(task, runs, now, opts.time);
|
|
522
527
|
const expressions = opts.expressions ?? {};
|
|
523
528
|
const strip = opts.still ? [] : compactStrip(task.state, metrics.slots, expressions, opts.theme);
|
|
524
|
-
const fixed = [...bannerLines(width), headerLine(task, now, quiet), ...strip];
|
|
529
|
+
const fixed = [...bannerLines(width), headerLine(task, now, quiet, opts.time), ...strip];
|
|
525
530
|
const tail = tailLines(task, runs, now, tick, steps, opts.theme);
|
|
526
531
|
const room = Math.max(0, MAX_PANEL_LINES - fixed.length - tail.length);
|
|
527
532
|
return [...fixed, ...tail, ...checklistLines(steps, room)].map((line) => clip(line, width));
|
|
@@ -577,8 +582,8 @@ function sceneInput(
|
|
|
577
582
|
steps: PlanStep[],
|
|
578
583
|
opts: PanelOptions,
|
|
579
584
|
): LargeSceneInput {
|
|
580
|
-
const metrics = sceneMetrics(task, runs, now);
|
|
581
|
-
const alert = taskAlert(task);
|
|
585
|
+
const metrics = sceneMetrics(task, runs, now, opts.time);
|
|
586
|
+
const alert = taskAlert(task, opts.time);
|
|
582
587
|
return {
|
|
583
588
|
taskId: task.id,
|
|
584
589
|
taskTitle: task.title,
|
|
@@ -616,6 +621,8 @@ export interface PanelOptions {
|
|
|
616
621
|
oracleMotion?: OracleMotion;
|
|
617
622
|
/** Leave out the oracle and agent animations: status, alert, activity and checklist only. */
|
|
618
623
|
still?: boolean;
|
|
624
|
+
/** The task's time budget: the elapsed label reads against it. */
|
|
625
|
+
time?: SceneTime;
|
|
619
626
|
}
|
|
620
627
|
|
|
621
628
|
/** The oracle's clocked animation state beyond its expression frame (see expressions.ts). */
|
package/src/roles/reviewer.ts
CHANGED
|
@@ -30,15 +30,23 @@ function parseVerdict(text: string | undefined): Verdict {
|
|
|
30
30
|
return "blocked";
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** The severity a finding was tagged with, or undefined when it carries none. */
|
|
34
|
+
function taggedSeverity(text: string): Severity | undefined {
|
|
35
|
+
const match = /^\[([^\]]+)\]/.exec(text);
|
|
36
|
+
return SEVERITIES.find((candidate) => match?.[1]?.toLowerCase().includes(candidate));
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
function parseFinding(text: string): ReviewFinding {
|
|
34
40
|
const match = /^\[([^\]]+)\]\s*(.*)$/.exec(text);
|
|
35
|
-
|
|
36
|
-
return { severity, text: (match?.[2] ?? text).trim() };
|
|
41
|
+
return { severity: taggedSeverity(text) ?? "info", text: (match?.[2] ?? text).trim() };
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/** The QA gate never passes by default: a PASS must cite at least one executed check. */
|
|
40
45
|
export const UNVERIFIED_PASS = "unverified pass: PASS without executed checks under ## Verification, downgraded to CHANGES_REQUIRED";
|
|
41
46
|
|
|
47
|
+
/** Minor and info findings never hold the gate; what the reviewer asked for is kept as follow-ups. */
|
|
48
|
+
export const MINOR_ONLY = "CHANGES_REQUIRED for minor or info findings only: passed, its asks kept as follow-ups";
|
|
49
|
+
|
|
42
50
|
/** A verification bullet names a command or check and its result (`- cmd — result`). */
|
|
43
51
|
function executedChecks(verification: string): string[] {
|
|
44
52
|
return bullets(verification).filter((entry) => /\S\s*(?:—|–|-{1,2}|:|=>|→)\s*\S/.test(entry));
|
|
@@ -49,23 +57,34 @@ export function parseReviewResult(domain: Domain, raw: string): ReviewResult {
|
|
|
49
57
|
const sections = parseSections(raw);
|
|
50
58
|
const verification = findSection(sections, "verification") ?? "";
|
|
51
59
|
const requiredChanges = bullets(findSection(sections, "required changes"));
|
|
60
|
+
const entries = bullets(findSection(sections, "findings"));
|
|
52
61
|
let verdict = parseVerdict(findSection(sections, "verdict"));
|
|
53
62
|
let downgraded: string | undefined;
|
|
54
|
-
|
|
63
|
+
let relaxed: string | undefined;
|
|
64
|
+
const checked = executedChecks(verification).length > 0;
|
|
65
|
+
if (verdict === "pass" && !checked) {
|
|
55
66
|
verdict = "changes_required";
|
|
56
67
|
downgraded = UNVERIFIED_PASS;
|
|
57
68
|
requiredChanges.push("Re-run the QA gate and record the checks actually executed, each as `- command — result`.");
|
|
69
|
+
} else if (
|
|
70
|
+
verdict === "changes_required" && checked && entries.length > 0
|
|
71
|
+
// Only findings explicitly tagged minor or info: an untagged one may be serious.
|
|
72
|
+
&& entries.every((entry) => { const severity = taggedSeverity(entry); return severity === "minor" || severity === "info"; })
|
|
73
|
+
) {
|
|
74
|
+
verdict = "pass";
|
|
75
|
+
relaxed = MINOR_ONLY;
|
|
58
76
|
}
|
|
59
77
|
return {
|
|
60
78
|
domain,
|
|
61
79
|
role: "reviewer",
|
|
62
80
|
verdict,
|
|
63
|
-
findings:
|
|
81
|
+
findings: entries.map(parseFinding),
|
|
64
82
|
verification,
|
|
65
83
|
requiredChanges,
|
|
66
84
|
optionalImprovements: bullets(findSection(sections, "optional improvements")),
|
|
67
85
|
pushback: parsePushback(sections),
|
|
68
86
|
...(downgraded ? { downgraded } : {}),
|
|
87
|
+
...(relaxed ? { relaxed } : {}),
|
|
69
88
|
raw,
|
|
70
89
|
};
|
|
71
90
|
}
|
package/src/roles/worker.ts
CHANGED
|
@@ -25,6 +25,8 @@ export const workerSpec: RoleSpec = {
|
|
|
25
25
|
"the matching section instead. Do not narrate. Report only what you actually changed and verified.",
|
|
26
26
|
"An optional `## Pushback` (`**Request:**`, `**Reason:**`, optional `**Alternative:**`) states a change you",
|
|
27
27
|
"believe is wrong, with your reason, instead of doing it; still complete everything else you safely can.",
|
|
28
|
+
"Stopped because your time is up, also add `## Left Off` (what you were doing, what is still to do) and",
|
|
29
|
+
"`## More Time` (`N minutes — why`).",
|
|
28
30
|
].join(" "),
|
|
29
31
|
};
|
|
30
32
|
|
|
@@ -65,6 +67,15 @@ export function parseBlockers(
|
|
|
65
67
|
];
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
/** `## More Time` as the minutes asked for (`15 minutes`, `1h`, `20m`) and the reason given. */
|
|
71
|
+
export function parseMoreTime(text: string): { minutes?: number; reason: string } {
|
|
72
|
+
const flat = text.replace(/^[-*]\s*/gm, "").replace(/\s+/g, " ").trim();
|
|
73
|
+
const match = /(\d+(?:\.\d+)?)\s*(h|hrs?|hours?|m|mins?|minutes?)\b/i.exec(flat);
|
|
74
|
+
const minutes = match ? Math.round(Number(match[1]) * (/^h/i.test(match[2]!) ? 60 : 1)) : undefined;
|
|
75
|
+
const reason = match ? flat.slice(match.index + match[0].length).replace(/^\s*(?:—|–|-|:|,|because|to)?\s*/i, "").trim() : flat;
|
|
76
|
+
return { ...(minutes && minutes > 0 ? { minutes } : {}), reason: reason || flat };
|
|
77
|
+
}
|
|
78
|
+
|
|
68
79
|
/** Parse a worker's markdown into a structured result (never throws). */
|
|
69
80
|
export function parseWorkerResult(domain: Domain, raw: string, now = new Date().toISOString()): WorkerResult {
|
|
70
81
|
const sections = parseSections(raw);
|
|
@@ -72,6 +83,7 @@ export function parseWorkerResult(domain: Domain, raw: string, now = new Date().
|
|
|
72
83
|
domain,
|
|
73
84
|
role: "worker",
|
|
74
85
|
completed: findSection(sections, "completed") ?? "",
|
|
86
|
+
...leftOffFields(sections),
|
|
75
87
|
filesChanged: bullets(findSection(sections, "files changed")).map((entry): FileChange => {
|
|
76
88
|
const file = parseFileBullet(entry);
|
|
77
89
|
return { path: file.path, change: file.reason };
|
|
@@ -87,6 +99,12 @@ export function parseWorkerResult(domain: Domain, raw: string, now = new Date().
|
|
|
87
99
|
};
|
|
88
100
|
}
|
|
89
101
|
|
|
102
|
+
function leftOffFields(sections: Map<string, string>): Pick<WorkerResult, "leftOff" | "moreTime"> {
|
|
103
|
+
const leftOff = findSection(sections, "left off")?.trim();
|
|
104
|
+
const more = findSection(sections, "more time")?.trim();
|
|
105
|
+
return { ...(leftOff ? { leftOff } : {}), ...(more ? { moreTime: parseMoreTime(more) } : {}) };
|
|
106
|
+
}
|
|
107
|
+
|
|
90
108
|
/** Report contract deviations so the Master does not accept unverified work. */
|
|
91
109
|
export function validateWorkerResult(result: WorkerResult): string[] {
|
|
92
110
|
const issues: string[] = [];
|
|
@@ -68,6 +68,8 @@ export interface WorkflowConfig {
|
|
|
68
68
|
maxParallelWorkers: number;
|
|
69
69
|
/** 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. */
|
|
70
70
|
freshContext: boolean;
|
|
71
|
+
/** Minutes of work time a new task gets unless it is started with its own (`--budget`); 0 = no budget. */
|
|
72
|
+
taskBudgetMinutes: number;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
export interface KnowledgeConfig {
|
|
@@ -204,6 +206,7 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
204
206
|
wrapUpAt: 0.75,
|
|
205
207
|
maxParallelWorkers: 3,
|
|
206
208
|
freshContext: true,
|
|
209
|
+
taskBudgetMinutes: 0,
|
|
207
210
|
},
|
|
208
211
|
knowledge: {
|
|
209
212
|
compactionThreshold: 20000,
|
|
@@ -340,6 +343,7 @@ export function resolveConfig(partial: unknown): BotLobbyConfig {
|
|
|
340
343
|
const src = (partial ?? {}) as Record<string, unknown>;
|
|
341
344
|
const workflow = { ...DEFAULT_CONFIG.workflow, ...(src.workflow as Partial<WorkflowConfig> | undefined) };
|
|
342
345
|
workflow.freshContext = workflow.freshContext !== false;
|
|
346
|
+
workflow.taskBudgetMinutes = typeof workflow.taskBudgetMinutes === "number" && workflow.taskBudgetMinutes > 0 ? Math.min(24 * 60, Math.round(workflow.taskBudgetMinutes)) : 0;
|
|
343
347
|
const knowledge = { ...DEFAULT_CONFIG.knowledge, ...(src.knowledge as Partial<KnowledgeConfig> | undefined) };
|
|
344
348
|
const srcAgents = (src.agents ?? {}) as Partial<BotLobbyConfig["agents"]>;
|
|
345
349
|
return {
|
package/src/schemas/findings.ts
CHANGED
|
@@ -42,6 +42,10 @@ export interface WorkerResult {
|
|
|
42
42
|
domain: Domain;
|
|
43
43
|
role: "worker";
|
|
44
44
|
completed: string;
|
|
45
|
+
/** Stopped for time: what it was doing and what is still to do. */
|
|
46
|
+
leftOff?: string;
|
|
47
|
+
/** Stopped for time: the minutes it asks for, and why. */
|
|
48
|
+
moreTime?: { minutes?: number; reason: string };
|
|
45
49
|
filesChanged: FileChange[];
|
|
46
50
|
verification: string;
|
|
47
51
|
notes: string;
|
|
@@ -69,6 +73,8 @@ export interface ReviewResult {
|
|
|
69
73
|
pushback?: Pushback;
|
|
70
74
|
/** Set when the engine downgraded an unsupported PASS. */
|
|
71
75
|
downgraded?: string;
|
|
76
|
+
/** Set when the engine passed a CHANGES_REQUIRED that named only minor or info findings. */
|
|
77
|
+
relaxed?: string;
|
|
72
78
|
raw: string;
|
|
73
79
|
}
|
|
74
80
|
|
|
@@ -137,4 +143,12 @@ export interface AgentRun {
|
|
|
137
143
|
routedFrom?: string;
|
|
138
144
|
/** Why and where it was routed (`trivial 0.88: p/big · medium → p/cheap · low`). */
|
|
139
145
|
route?: string;
|
|
146
|
+
/** Files the run changed with `edit`/`write`, absolute, across its attempts. */
|
|
147
|
+
edited?: string[];
|
|
148
|
+
/** Under a task time budget: the time it was given, when that runs out (epoch ms), and any granted since. */
|
|
149
|
+
allotMs?: number;
|
|
150
|
+
endsAt?: number;
|
|
151
|
+
extendedMs?: number;
|
|
152
|
+
/** Stopped at its allotted time and not given more: its report says where it left off. */
|
|
153
|
+
timeUp?: boolean;
|
|
140
154
|
}
|