@bubblebrain-ai/bubble 0.0.37 → 0.0.39
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/dist/agent/internal-reminder-sanitizer.js +4 -0
- package/dist/agent/profiles.d.ts +6 -0
- package/dist/agent/profiles.js +32 -0
- package/dist/agent/task-lifecycle-reminder.d.ts +26 -0
- package/dist/agent/task-lifecycle-reminder.js +59 -0
- package/dist/agent.d.ts +43 -0
- package/dist/agent.js +70 -0
- package/dist/approval/controller.d.ts +3 -1
- package/dist/approval/controller.js +5 -2
- package/dist/approval/tool-helper.js +10 -1
- package/dist/approval/types.d.ts +15 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +4 -0
- package/dist/loop/engine.d.ts +29 -0
- package/dist/loop/engine.js +63 -0
- package/dist/main.js +92 -24
- package/dist/orchestrator/default-hooks.js +94 -1
- package/dist/orchestrator/hooks.d.ts +7 -0
- package/dist/print-output.d.ts +54 -0
- package/dist/print-output.js +103 -0
- package/dist/prompt/delegation.js +6 -3
- package/dist/prompt/task-reminders.d.ts +17 -0
- package/dist/prompt/task-reminders.js +15 -0
- package/dist/session-types.d.ts +1 -1
- package/dist/tasks/manager.d.ts +136 -0
- package/dist/tasks/manager.js +530 -0
- package/dist/tasks/process-utils.d.ts +10 -0
- package/dist/tasks/process-utils.js +49 -0
- package/dist/tasks/promotion.d.ts +17 -0
- package/dist/tasks/promotion.js +30 -0
- package/dist/tasks/wake.d.ts +53 -0
- package/dist/tasks/wake.js +125 -0
- package/dist/tools/bash.d.ts +15 -1
- package/dist/tools/bash.js +103 -3
- package/dist/tools/child-tools.js +3 -1
- package/dist/tools/edit.js +4 -12
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +8 -1
- package/dist/tools/server-manager.d.ts +6 -36
- package/dist/tools/server-manager.js +6 -234
- package/dist/tools/task-tools.d.ts +10 -0
- package/dist/tools/task-tools.js +129 -0
- package/dist/tools/write.js +6 -12
- package/dist/tui-ink/app.d.ts +20 -2
- package/dist/tui-ink/app.js +307 -19
- package/dist/tui-ink/approval/approval-dialog.js +4 -4
- package/dist/tui-ink/display-history.d.ts +1 -1
- package/dist/tui-ink/footer.d.ts +3 -3
- package/dist/tui-ink/footer.js +3 -3
- package/dist/tui-ink/input-box.d.ts +6 -0
- package/dist/tui-ink/markdown.d.ts +10 -2
- package/dist/tui-ink/markdown.js +13 -12
- package/dist/tui-ink/message-list.js +14 -3
- package/dist/tui-ink/run.d.ts +6 -0
- package/dist/tui-ink/run.js +1 -1
- package/dist/tui-ink/subagent-inspector.d.ts +5 -1
- package/dist/tui-ink/subagent-inspector.js +23 -7
- package/dist/tui-ink/welcome.js +4 -0
- package/package.json +1 -1
|
@@ -167,6 +167,10 @@ const INTERNAL_REASONING_REFERENCE_PATTERNS = [
|
|
|
167
167
|
/Subagent lifecycle truth/i,
|
|
168
168
|
/Count unique agent_id values only/i,
|
|
169
169
|
/Do not describe a subagent as running or still working/i,
|
|
170
|
+
/Background task truth/i,
|
|
171
|
+
/Never re-run work a finished task already did/i,
|
|
172
|
+
/Large-change checkpoint/i,
|
|
173
|
+
/delegation only adds merge risk/i,
|
|
170
174
|
];
|
|
171
175
|
function consumeInternalBlockAtStart(text, final) {
|
|
172
176
|
if (text.startsWith(INTERNAL_TAG_PREFIX)) {
|
package/dist/agent/profiles.d.ts
CHANGED
|
@@ -56,6 +56,12 @@ export interface AgentProfileDiagnostic {
|
|
|
56
56
|
export type AgentProfileScope = "user" | "project" | "both";
|
|
57
57
|
export declare function discoverAgentProfiles(cwd: string, scope?: AgentProfileScope): DiscoverAgentProfilesResult;
|
|
58
58
|
export declare function builtinAgentProfiles(): AgentProfile[];
|
|
59
|
+
/**
|
|
60
|
+
* The delegation nudge only fires when a write-capable spawn surface exists
|
|
61
|
+
* (large-task-delegation design §3): defense in depth for hosts that strip
|
|
62
|
+
* or override the builtin profiles.
|
|
63
|
+
*/
|
|
64
|
+
export declare function hasWriteWorktreeProfile(profiles: AgentProfile[]): boolean;
|
|
59
65
|
export declare function findAgentProfile(profiles: AgentProfile[], name: string): AgentProfile | undefined;
|
|
60
66
|
export declare function assignAgentNickname(profile: AgentProfile, activeNicknames?: Iterable<string>): string;
|
|
61
67
|
/**
|
package/dist/agent/profiles.js
CHANGED
|
@@ -134,8 +134,40 @@ export function builtinAgentProfiles() {
|
|
|
134
134
|
toProfile("security_investigation"),
|
|
135
135
|
toProfile("evidence_correlation"),
|
|
136
136
|
toProfile("general_readonly"),
|
|
137
|
+
// The write-capable builtin (large-task-delegation design §3): before
|
|
138
|
+
// this, write_worktree was reachable only via user-authored profile
|
|
139
|
+
// frontmatter, so the delegation nudge would have recommended a surface
|
|
140
|
+
// that does not exist in a stock session.
|
|
141
|
+
{
|
|
142
|
+
name: "implementer",
|
|
143
|
+
description: "Write-capable implementation subagent (isolated git worktree)",
|
|
144
|
+
source: "builtin",
|
|
145
|
+
mode: "write_worktree",
|
|
146
|
+
model: "inherit",
|
|
147
|
+
tools: {
|
|
148
|
+
preset: "explicit",
|
|
149
|
+
include: ["read", "glob", "grep", "edit", "write", "bash", "lsp", "todo_write"],
|
|
150
|
+
exclude: [],
|
|
151
|
+
},
|
|
152
|
+
approval: "fail",
|
|
153
|
+
nicknameCandidates: DEFAULT_NICKNAME_CANDIDATES,
|
|
154
|
+
prompt: [
|
|
155
|
+
"You are an implementer subagent working in your own isolated git worktree — the parent tree is never touched.",
|
|
156
|
+
"Your worktree forks from the last commit; changes the parent made after that commit are NOT present unless included in your briefing.",
|
|
157
|
+
"Make the smallest coherent edits that complete your assigned group. Stay strictly inside the file set you were given; if a needed change falls outside it, report that instead of editing.",
|
|
158
|
+
"Verify your changes narrowly (targeted tests or typecheck) when possible, and end with a concise summary of files changed and verification results — the parent reviews and applies your worktree.",
|
|
159
|
+
].join("\n"),
|
|
160
|
+
},
|
|
137
161
|
];
|
|
138
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* The delegation nudge only fires when a write-capable spawn surface exists
|
|
165
|
+
* (large-task-delegation design §3): defense in depth for hosts that strip
|
|
166
|
+
* or override the builtin profiles.
|
|
167
|
+
*/
|
|
168
|
+
export function hasWriteWorktreeProfile(profiles) {
|
|
169
|
+
return profiles.some((profile) => profile.mode === "write_worktree");
|
|
170
|
+
}
|
|
139
171
|
export function findAgentProfile(profiles, name) {
|
|
140
172
|
return profiles.find((profile) => profile.name === name)
|
|
141
173
|
?? profiles.find((profile) => profile.name === `builtin:${name}`);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background-task lifecycle reminder (background-tasks design §2.3a).
|
|
3
|
+
*
|
|
4
|
+
* Emission is STATE-CHANGE gated, never per-model-call: the reminder channel
|
|
5
|
+
* is append-only (each injection is a persistent meta message that survives
|
|
6
|
+
* pruning and compaction), so the caller compares the manager's task state
|
|
7
|
+
* version and builds a reminder only when the owned task set changed since
|
|
8
|
+
* the last emission. This mirrors the subagent lifecycle reminder's activity
|
|
9
|
+
* gate rather than firing while tasks are merely alive.
|
|
10
|
+
*
|
|
11
|
+
* Because compaction preserves meta messages but drops session markers, this
|
|
12
|
+
* reminder is also how the model recovers task ids after /compact.
|
|
13
|
+
*/
|
|
14
|
+
import type { BackgroundTaskInfo } from "../tasks/manager.js";
|
|
15
|
+
export interface TaskReminderInput {
|
|
16
|
+
tasks: BackgroundTaskInfo[];
|
|
17
|
+
outputTail: (id: string) => string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* The task tools are already unlocked (the spawn path calls
|
|
20
|
+
* unlockDeferredTools). When true the tool_search hint is dropped — field
|
|
21
|
+
* test 2026-07-19 showed the unconditional hint makes the model burn a
|
|
22
|
+
* tool_search call for tools it already has.
|
|
23
|
+
*/
|
|
24
|
+
toolsAvailable?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function buildTaskLifecycleReminder({ tasks, outputTail, toolsAvailable }: TaskReminderInput): string | undefined;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background-task lifecycle reminder (background-tasks design §2.3a).
|
|
3
|
+
*
|
|
4
|
+
* Emission is STATE-CHANGE gated, never per-model-call: the reminder channel
|
|
5
|
+
* is append-only (each injection is a persistent meta message that survives
|
|
6
|
+
* pruning and compaction), so the caller compares the manager's task state
|
|
7
|
+
* version and builds a reminder only when the owned task set changed since
|
|
8
|
+
* the last emission. This mirrors the subagent lifecycle reminder's activity
|
|
9
|
+
* gate rather than firing while tasks are merely alive.
|
|
10
|
+
*
|
|
11
|
+
* Because compaction preserves meta messages but drops session markers, this
|
|
12
|
+
* reminder is also how the model recovers task ids after /compact.
|
|
13
|
+
*/
|
|
14
|
+
const OUTPUT_TAIL_CHARS = 2000;
|
|
15
|
+
export function buildTaskLifecycleReminder({ tasks, outputTail, toolsAvailable }) {
|
|
16
|
+
if (tasks.length === 0)
|
|
17
|
+
return undefined;
|
|
18
|
+
const running = tasks.filter((task) => task.status === "running");
|
|
19
|
+
const finishedUndelivered = tasks.filter((task) => task.status !== "running" && task.deliveredAt === undefined);
|
|
20
|
+
const finishedDelivered = tasks.filter((task) => task.status !== "running" && task.deliveredAt !== undefined);
|
|
21
|
+
if (running.length === 0 && finishedUndelivered.length === 0 && finishedDelivered.length === 0) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const lines = ["Background task truth:"];
|
|
25
|
+
for (const task of running) {
|
|
26
|
+
const elapsed = Math.round((Date.now() - task.startedAt) / 1000);
|
|
27
|
+
lines.push(`- running: ${task.id} (${label(task)}, ${elapsed}s elapsed)`);
|
|
28
|
+
}
|
|
29
|
+
for (const task of finishedUndelivered) {
|
|
30
|
+
const elapsed = Math.round(((task.endedAt ?? Date.now()) - task.startedAt) / 1000);
|
|
31
|
+
lines.push(`- finished since last update: ${task.id} (${label(task)}) ${task.status}${task.exitCode != null ? ` exit ${task.exitCode}` : ""} in ${elapsed}s`);
|
|
32
|
+
const tail = outputTail(task.id)?.trim();
|
|
33
|
+
if (tail) {
|
|
34
|
+
lines.push(` output tail:`);
|
|
35
|
+
lines.push(...clampTail(tail).split("\n").map((line) => ` ${line}`));
|
|
36
|
+
lines.push(" Call task_output only if you need more of the output.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Delivered final results already reached the model in full — id + status
|
|
40
|
+
// only, never the output again (mirrors the subagent delivered demotion).
|
|
41
|
+
for (const task of finishedDelivered) {
|
|
42
|
+
lines.push(`- delivered: ${task.id} ${task.status}${task.exitCode != null ? ` (exit ${task.exitCode})` : ""}`);
|
|
43
|
+
}
|
|
44
|
+
lines.push(toolsAvailable
|
|
45
|
+
? "- Check tasks with task_output; stop them with kill_task. Both are already in your tool list — do NOT call tool_search for them."
|
|
46
|
+
: "- Check tasks with task_output; stop them with kill_task (deferred: load via tool_search with query 'select:task_output,kill_task' if not yet available).");
|
|
47
|
+
lines.push("- Never re-run work a finished task already did; never poll with foreground sleep.");
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
function label(task) {
|
|
51
|
+
return task.description?.trim() || truncate(task.command, 60);
|
|
52
|
+
}
|
|
53
|
+
function clampTail(tail) {
|
|
54
|
+
return tail.length <= OUTPUT_TAIL_CHARS ? tail : `…${tail.slice(-OUTPUT_TAIL_CHARS)}`;
|
|
55
|
+
}
|
|
56
|
+
function truncate(value, max) {
|
|
57
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
58
|
+
return oneLine.length <= max ? oneLine : `${oneLine.slice(0, max - 1)}…`;
|
|
59
|
+
}
|
package/dist/agent.d.ts
CHANGED
|
@@ -101,6 +101,25 @@ export declare class Agent {
|
|
|
101
101
|
messages: Message[];
|
|
102
102
|
private provider;
|
|
103
103
|
private sessionID?;
|
|
104
|
+
/**
|
|
105
|
+
* Bridge to the unified process manager's background tasks, wired by the
|
|
106
|
+
* host (main.ts) when the host supports them (background-tasks design
|
|
107
|
+
* §2.3a). list() is pre-filtered to the agent's bound session.
|
|
108
|
+
*/
|
|
109
|
+
backgroundTasks?: {
|
|
110
|
+
list: () => import("./tasks/manager.js").BackgroundTaskInfo[];
|
|
111
|
+
version: () => number;
|
|
112
|
+
outputTail: (id: string) => string | undefined;
|
|
113
|
+
};
|
|
114
|
+
private lastTaskReminderVersion;
|
|
115
|
+
/**
|
|
116
|
+
* Once-per-session latch for the large-task delegation nudge
|
|
117
|
+
* (large-task-delegation design §2): goal-loop continuations rebuild
|
|
118
|
+
* TurnHookState every run, so a per-turn latch would nag on every turn.
|
|
119
|
+
* If the model read the nudge once and chose not to delegate, repeating
|
|
120
|
+
* it is noise.
|
|
121
|
+
*/
|
|
122
|
+
largeTaskNudgeConsumed: boolean;
|
|
104
123
|
private _providerId;
|
|
105
124
|
private _model;
|
|
106
125
|
private tools;
|
|
@@ -165,6 +184,21 @@ export declare class Agent {
|
|
|
165
184
|
* the prompt's tool list.
|
|
166
185
|
*/
|
|
167
186
|
injectDeferredToolsReminder(): void;
|
|
187
|
+
/**
|
|
188
|
+
* Rebinds the agent to a new session (TUI session switch). Keeps
|
|
189
|
+
* ownerSessionId on newly spawned background tasks and managed servers
|
|
190
|
+
* accurate (background-tasks design §2.2c).
|
|
191
|
+
*/
|
|
192
|
+
setSessionID(sessionID: string | undefined): void;
|
|
193
|
+
getSessionID(): string | undefined;
|
|
194
|
+
/**
|
|
195
|
+
* Background-task reminder, state-change gated (design §2.3a): returns a
|
|
196
|
+
* reminder only when the owned task set changed since the last emission
|
|
197
|
+
* (started/finished/killed). The reminder channel is append-only, so
|
|
198
|
+
* emitting on every model call while tasks are merely alive would stack
|
|
199
|
+
* persistent duplicates that neither pruning nor compaction removes.
|
|
200
|
+
*/
|
|
201
|
+
consumeBackgroundTaskReminder(): string | undefined;
|
|
168
202
|
private runExternalHook;
|
|
169
203
|
private injectHookModelContext;
|
|
170
204
|
/** Whether a tool is registered on this agent (e.g. delegation tools on parents). */
|
|
@@ -269,6 +303,15 @@ export declare class Agent {
|
|
|
269
303
|
abortSignal?: AbortSignal;
|
|
270
304
|
}): Promise<SubagentThreadSnapshot>;
|
|
271
305
|
closeSubAgent(agentId: string): Promise<SubagentThreadSnapshot>;
|
|
306
|
+
/**
|
|
307
|
+
* Live (non-final) children only — the delegation-nudge gate
|
|
308
|
+
* (large-task-delegation design §2). listSubAgents() is a grows-only
|
|
309
|
+
* session history (finished + resumed children stay forever), so it must
|
|
310
|
+
* never be used to answer "am I currently delegating?".
|
|
311
|
+
*/
|
|
312
|
+
activeSubAgentCount(): number;
|
|
313
|
+
/** run_workflow children are workflowInternal and invisible to listSubAgents. */
|
|
314
|
+
hasRunningWorkflow(): boolean;
|
|
272
315
|
listSubAgents(): SubagentThreadSnapshot[];
|
|
273
316
|
/**
|
|
274
317
|
* Dynamic workflow (option C): runs an LLM-authored JS orchestration script in
|
package/dist/agent.js
CHANGED
|
@@ -19,6 +19,8 @@ import { buildDeferredToolsReminder, buildToolFreezeReminder, reminderForMode }
|
|
|
19
19
|
import { HookBus } from "./orchestrator/hooks.js";
|
|
20
20
|
import { normalizeHookInput, truncateHookText, } from "./hooks/index.js";
|
|
21
21
|
import { createDefaultHooks } from "./orchestrator/default-hooks.js";
|
|
22
|
+
import { buildTaskLifecycleReminder } from "./agent/task-lifecycle-reminder.js";
|
|
23
|
+
import { classifyTask } from "./agent/task-classifier.js";
|
|
22
24
|
import { mergeAgentCategories, parseThinkingLevel, resolveModelRoute, resolveSubagentRoute } from "./agent/categories.js";
|
|
23
25
|
import { DEFAULT_AGENT_ROUTING, nearModelMatches, sanitizeAgentRouting, tierContextFromSnapshot } from "./agent/routing-catalog.js";
|
|
24
26
|
import { buildModelRoutingPrompt } from "./prompt/routing.js";
|
|
@@ -100,6 +102,21 @@ export class Agent {
|
|
|
100
102
|
messages = [];
|
|
101
103
|
provider;
|
|
102
104
|
sessionID;
|
|
105
|
+
/**
|
|
106
|
+
* Bridge to the unified process manager's background tasks, wired by the
|
|
107
|
+
* host (main.ts) when the host supports them (background-tasks design
|
|
108
|
+
* §2.3a). list() is pre-filtered to the agent's bound session.
|
|
109
|
+
*/
|
|
110
|
+
backgroundTasks;
|
|
111
|
+
lastTaskReminderVersion = -1;
|
|
112
|
+
/**
|
|
113
|
+
* Once-per-session latch for the large-task delegation nudge
|
|
114
|
+
* (large-task-delegation design §2): goal-loop continuations rebuild
|
|
115
|
+
* TurnHookState every run, so a per-turn latch would nag on every turn.
|
|
116
|
+
* If the model read the nudge once and chose not to delegate, repeating
|
|
117
|
+
* it is noise.
|
|
118
|
+
*/
|
|
119
|
+
largeTaskNudgeConsumed = false;
|
|
103
120
|
_providerId;
|
|
104
121
|
_model;
|
|
105
122
|
tools = new Map();
|
|
@@ -261,6 +278,37 @@ export class Agent {
|
|
|
261
278
|
this.injectSystemReminder(buildDeferredToolsReminder(deferredNames));
|
|
262
279
|
}
|
|
263
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Rebinds the agent to a new session (TUI session switch). Keeps
|
|
283
|
+
* ownerSessionId on newly spawned background tasks and managed servers
|
|
284
|
+
* accurate (background-tasks design §2.2c).
|
|
285
|
+
*/
|
|
286
|
+
setSessionID(sessionID) {
|
|
287
|
+
this.sessionID = sessionID;
|
|
288
|
+
}
|
|
289
|
+
getSessionID() {
|
|
290
|
+
return this.sessionID;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Background-task reminder, state-change gated (design §2.3a): returns a
|
|
294
|
+
* reminder only when the owned task set changed since the last emission
|
|
295
|
+
* (started/finished/killed). The reminder channel is append-only, so
|
|
296
|
+
* emitting on every model call while tasks are merely alive would stack
|
|
297
|
+
* persistent duplicates that neither pruning nor compaction removes.
|
|
298
|
+
*/
|
|
299
|
+
consumeBackgroundTaskReminder() {
|
|
300
|
+
if (!this.backgroundTasks)
|
|
301
|
+
return undefined;
|
|
302
|
+
const version = this.backgroundTasks.version();
|
|
303
|
+
if (version === this.lastTaskReminderVersion)
|
|
304
|
+
return undefined;
|
|
305
|
+
this.lastTaskReminderVersion = version;
|
|
306
|
+
return buildTaskLifecycleReminder({
|
|
307
|
+
tasks: this.backgroundTasks.list(),
|
|
308
|
+
outputTail: (id) => this.backgroundTasks?.outputTail(id),
|
|
309
|
+
toolsAvailable: this.unlockedDeferred?.has("task_output") ?? false,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
264
312
|
async runExternalHook(request, abortSignal) {
|
|
265
313
|
const events = [];
|
|
266
314
|
if (!this.externalHooks) {
|
|
@@ -509,6 +557,15 @@ export class Agent {
|
|
|
509
557
|
events.push(...hook.events);
|
|
510
558
|
this.injectHookModelContext(hook.result);
|
|
511
559
|
this.appendMessage({ role: "user", content: input.content });
|
|
560
|
+
// Large-task detector (large-task-delegation design §1): a steer
|
|
561
|
+
// redirects the task — stale breadth evidence must not fire a nudge
|
|
562
|
+
// about the pre-steer task, and the classifier should reason about
|
|
563
|
+
// the redirected one.
|
|
564
|
+
hookState.exploredFiles?.clear();
|
|
565
|
+
hookState.largeTaskCheckpointDone = false;
|
|
566
|
+
if (typeof input.content === "string" && input.content.trim()) {
|
|
567
|
+
hookState.taskType = classifyTask(input.content);
|
|
568
|
+
}
|
|
512
569
|
events.push({
|
|
513
570
|
type: "input_applied",
|
|
514
571
|
id: input.id,
|
|
@@ -1636,6 +1693,19 @@ export class Agent {
|
|
|
1636
1693
|
this.subagentStore.notifyWaiters(record);
|
|
1637
1694
|
return this.snapshotSubagent(record);
|
|
1638
1695
|
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Live (non-final) children only — the delegation-nudge gate
|
|
1698
|
+
* (large-task-delegation design §2). listSubAgents() is a grows-only
|
|
1699
|
+
* session history (finished + resumed children stay forever), so it must
|
|
1700
|
+
* never be used to answer "am I currently delegating?".
|
|
1701
|
+
*/
|
|
1702
|
+
activeSubAgentCount() {
|
|
1703
|
+
return this.subagentStore.activeCount();
|
|
1704
|
+
}
|
|
1705
|
+
/** run_workflow children are workflowInternal and invisible to listSubAgents. */
|
|
1706
|
+
hasRunningWorkflow() {
|
|
1707
|
+
return [...this.workflowRuns.values()].some((record) => record.status === "running");
|
|
1708
|
+
}
|
|
1639
1709
|
listSubAgents() {
|
|
1640
1710
|
return this.subagentStore.values()
|
|
1641
1711
|
.filter((record) => !record.workflowInternal)
|
|
@@ -33,7 +33,9 @@ export interface ApprovalControllerOptions {
|
|
|
33
33
|
*
|
|
34
34
|
* deny rule match → reject (applies even under bypassPermissions)
|
|
35
35
|
* bypassPermissions → auto-approve, no prompt
|
|
36
|
-
* default + edit|write → auto-approve
|
|
36
|
+
* default + edit|write → auto-approve (workspace paths only; a
|
|
37
|
+
* path outside the workspace falls through
|
|
38
|
+
* to allow rules / the interactive prompt)
|
|
37
39
|
* plan → reject with instructions to use exit_plan_mode
|
|
38
40
|
* allow rule match → auto-approve
|
|
39
41
|
* bash in session allowlist → auto-approve
|
|
@@ -5,7 +5,9 @@ import { truncateHookText } from "../hooks/index.js";
|
|
|
5
5
|
*
|
|
6
6
|
* deny rule match → reject (applies even under bypassPermissions)
|
|
7
7
|
* bypassPermissions → auto-approve, no prompt
|
|
8
|
-
* default + edit|write → auto-approve
|
|
8
|
+
* default + edit|write → auto-approve (workspace paths only; a
|
|
9
|
+
* path outside the workspace falls through
|
|
10
|
+
* to allow rules / the interactive prompt)
|
|
9
11
|
* plan → reject with instructions to use exit_plan_mode
|
|
10
12
|
* allow rule match → auto-approve
|
|
11
13
|
* bash in session allowlist → auto-approve
|
|
@@ -46,7 +48,8 @@ export class PermissionAwareApprovalController {
|
|
|
46
48
|
if (mode === "bypassPermissions") {
|
|
47
49
|
return finalize({ action: "approve" });
|
|
48
50
|
}
|
|
49
|
-
|
|
51
|
+
const outsideWorkspace = (req.type === "edit" || req.type === "write") && req.outsideWorkspace === true;
|
|
52
|
+
if (mode === "default" && !outsideWorkspace && (req.type === "edit" || req.type === "write" || req.type === "patch")) {
|
|
50
53
|
return finalize({ action: "approve" });
|
|
51
54
|
}
|
|
52
55
|
// Project profile trust is a user decision, not a destructive action:
|
|
@@ -16,7 +16,16 @@ export async function gateToolAction(approval, req) {
|
|
|
16
16
|
const message = feedback
|
|
17
17
|
? `${label} was rejected by the user. User feedback: ${feedback}`
|
|
18
18
|
: `${label} was rejected by the user.`;
|
|
19
|
-
return {
|
|
19
|
+
return {
|
|
20
|
+
approved: false,
|
|
21
|
+
result: {
|
|
22
|
+
content: message,
|
|
23
|
+
isError: true,
|
|
24
|
+
// Surfaces the rejection reason in subagent tool notes (which prefer
|
|
25
|
+
// metadata.reason over the truncated first content line).
|
|
26
|
+
metadata: { kind: "security", reason: feedback || message },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
20
29
|
}
|
|
21
30
|
function approvalRequestLabel(req) {
|
|
22
31
|
switch (req.type) {
|
package/dist/approval/types.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface EditApprovalRequest {
|
|
|
12
12
|
/** Unified patch (with context lines) to show the user. */
|
|
13
13
|
diff: string;
|
|
14
14
|
fileExists: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The path resolves outside the session workspace. Escalates the request
|
|
17
|
+
* past default-mode auto-approval so the user explicitly confirms it
|
|
18
|
+
* (bypassPermissions still auto-approves, consistent with bash).
|
|
19
|
+
*/
|
|
20
|
+
outsideWorkspace?: boolean;
|
|
15
21
|
}
|
|
16
22
|
export interface WriteApprovalRequest {
|
|
17
23
|
type: "write";
|
|
@@ -21,6 +27,8 @@ export interface WriteApprovalRequest {
|
|
|
21
27
|
/** Unified patch from existing contents to pending contents. */
|
|
22
28
|
diff?: string;
|
|
23
29
|
fileExists: boolean;
|
|
30
|
+
/** See EditApprovalRequest.outsideWorkspace. */
|
|
31
|
+
outsideWorkspace?: boolean;
|
|
24
32
|
}
|
|
25
33
|
export interface PatchApprovalRequest {
|
|
26
34
|
type: "patch";
|
|
@@ -39,6 +47,13 @@ export interface BashApprovalRequest {
|
|
|
39
47
|
type: "bash";
|
|
40
48
|
command: string;
|
|
41
49
|
cwd: string;
|
|
50
|
+
/**
|
|
51
|
+
* The command will run as a background task that outlives the turn
|
|
52
|
+
* (background-tasks design §2.4). Shown in the approval dialog and passed
|
|
53
|
+
* to permission hooks so org policy can discriminate; allow rules, the
|
|
54
|
+
* session allowlist, and bypass approve it like foreground by design.
|
|
55
|
+
*/
|
|
56
|
+
background?: boolean;
|
|
42
57
|
}
|
|
43
58
|
export interface LspApprovalRequest {
|
|
44
59
|
type: "lsp";
|
package/dist/cli.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface CliArgs {
|
|
|
11
11
|
resume?: boolean;
|
|
12
12
|
sessionName?: string;
|
|
13
13
|
print?: boolean;
|
|
14
|
+
/** Print-mode output: plain (default) or a single JSON object on stdout. */
|
|
15
|
+
outputFormat?: "plain" | "json";
|
|
14
16
|
prompt?: string;
|
|
15
17
|
thinkingLevel?: ThinkingLevel;
|
|
16
18
|
mode?: PermissionMode;
|
package/dist/cli.js
CHANGED
|
@@ -55,6 +55,15 @@ export function parseArgs(argv) {
|
|
|
55
55
|
case "-p":
|
|
56
56
|
args.print = true;
|
|
57
57
|
break;
|
|
58
|
+
case "--output-format": {
|
|
59
|
+
const value = argv[++i];
|
|
60
|
+
if (value !== "plain" && value !== "json") {
|
|
61
|
+
console.error(`Invalid --output-format: ${value ?? "(missing)"}. Expected plain or json.`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
args.outputFormat = value;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
58
67
|
case "--plan":
|
|
59
68
|
args.mode = "plan";
|
|
60
69
|
break;
|
|
@@ -108,6 +117,8 @@ Options (default):
|
|
|
108
117
|
--dangerously-skip-permissions
|
|
109
118
|
Enable bypass mode (auto-approve EVERY tool; disables all safety prompts)
|
|
110
119
|
-p, --print Non-interactive mode (single prompt)
|
|
120
|
+
--output-format <fmt> Print-mode output: plain (default) or json
|
|
121
|
+
(one JSON object on stdout: text, usage, num_turns)
|
|
111
122
|
-v, --version Print the installed version and exit
|
|
112
123
|
-h, --help Show this help
|
|
113
124
|
|
package/dist/config.d.ts
CHANGED
|
@@ -35,6 +35,10 @@ export interface UserConfigData {
|
|
|
35
35
|
/** Subagent model-routing knobs: autoTier (default true), allowCrossProvider (default true). */
|
|
36
36
|
agentRouting?: Partial<AgentRoutingConfig>;
|
|
37
37
|
subagents?: SubagentsUserConfig;
|
|
38
|
+
/** Background tasks (design §2.3b): autoResume defaults ON; false keeps notices + reminders only. */
|
|
39
|
+
tasks?: {
|
|
40
|
+
autoResume?: boolean;
|
|
41
|
+
};
|
|
38
42
|
}
|
|
39
43
|
export interface SubagentsUserConfig {
|
|
40
44
|
/** Global cap on concurrently running children. Default 8. */
|
|
@@ -50,6 +54,8 @@ export declare class UserConfig {
|
|
|
50
54
|
getDefaultThinkingLevel(): ThinkingLevel | undefined;
|
|
51
55
|
setDefaultThinkingLevel(level: ThinkingLevel): void;
|
|
52
56
|
getRecentModels(): string[];
|
|
57
|
+
/** Auto-resume on background-task completion (design §2.3b). Default ON. */
|
|
58
|
+
getTasksAutoResume(): boolean;
|
|
53
59
|
pushRecentModel(model: string): void;
|
|
54
60
|
getApiKey(): string | undefined;
|
|
55
61
|
setApiKey(key: string): void;
|
package/dist/config.js
CHANGED
|
@@ -139,6 +139,10 @@ export class UserConfig {
|
|
|
139
139
|
getRecentModels() {
|
|
140
140
|
return sanitizeRecentModels(this.data.recentModels)?.slice() ?? [];
|
|
141
141
|
}
|
|
142
|
+
/** Auto-resume on background-task completion (design §2.3b). Default ON. */
|
|
143
|
+
getTasksAutoResume() {
|
|
144
|
+
return this.data.tasks?.autoResume !== false;
|
|
145
|
+
}
|
|
142
146
|
pushRecentModel(model) {
|
|
143
147
|
if (isHiddenProviderId(modelProviderId(model))) {
|
|
144
148
|
return;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /loop — recurring prompts (background-tasks design §2.6).
|
|
3
|
+
*
|
|
4
|
+
* Pure decision logic kept out of the TUI, mirroring goal/engine.ts: the TUI
|
|
5
|
+
* owns a 1s ticker and calls decideLoopFiring per loop; this module owns
|
|
6
|
+
* parsing, caps, and the defer-not-stack rule (a firing due while a turn is
|
|
7
|
+
* running reschedules with a notice instead of piling up).
|
|
8
|
+
*/
|
|
9
|
+
export interface LoopState {
|
|
10
|
+
id: number;
|
|
11
|
+
prompt: string;
|
|
12
|
+
intervalMs: number;
|
|
13
|
+
nextFireAt: number;
|
|
14
|
+
fires: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const MAX_ACTIVE_LOOPS = 5;
|
|
17
|
+
export declare const MIN_LOOP_INTERVAL_MS = 60000;
|
|
18
|
+
export interface ParsedLoopCommand {
|
|
19
|
+
action: "start" | "list" | "stop" | "help";
|
|
20
|
+
intervalMs?: number;
|
|
21
|
+
prompt?: string;
|
|
22
|
+
stopId?: number;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function parseLoopCommand(raw: string): ParsedLoopCommand;
|
|
26
|
+
export type LoopFiringDecision = "fire" | "defer" | "wait";
|
|
27
|
+
export declare function decideLoopFiring(loop: LoopState, now: number, turnRunning: boolean): LoopFiringDecision;
|
|
28
|
+
export declare function formatLoopList(loops: LoopState[], now: number): string;
|
|
29
|
+
export declare function formatInterval(intervalMs: number): string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /loop — recurring prompts (background-tasks design §2.6).
|
|
3
|
+
*
|
|
4
|
+
* Pure decision logic kept out of the TUI, mirroring goal/engine.ts: the TUI
|
|
5
|
+
* owns a 1s ticker and calls decideLoopFiring per loop; this module owns
|
|
6
|
+
* parsing, caps, and the defer-not-stack rule (a firing due while a turn is
|
|
7
|
+
* running reschedules with a notice instead of piling up).
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_ACTIVE_LOOPS = 5;
|
|
10
|
+
export const MIN_LOOP_INTERVAL_MS = 60_000;
|
|
11
|
+
export function parseLoopCommand(raw) {
|
|
12
|
+
const rest = raw.trim().replace(/^\/loop\s*/, "").trim();
|
|
13
|
+
if (!rest)
|
|
14
|
+
return { action: "help" };
|
|
15
|
+
if (rest === "list")
|
|
16
|
+
return { action: "list" };
|
|
17
|
+
const stop = rest.match(/^stop(?:\s+(\d+))?$/);
|
|
18
|
+
if (stop) {
|
|
19
|
+
return { action: "stop", stopId: stop[1] ? Number(stop[1]) : undefined };
|
|
20
|
+
}
|
|
21
|
+
const match = rest.match(/^(\d+)([smh])\s+(.+)$/s);
|
|
22
|
+
if (!match) {
|
|
23
|
+
return {
|
|
24
|
+
action: "start",
|
|
25
|
+
error: "Usage: /loop <interval> <prompt> — interval like 90s, 5m, 1h (min 60s). Also: /loop list, /loop stop [id].",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const value = Number(match[1]);
|
|
29
|
+
const unit = match[2];
|
|
30
|
+
const multiplier = unit === "s" ? 1000 : unit === "m" ? 60_000 : 3_600_000;
|
|
31
|
+
const intervalMs = Math.max(MIN_LOOP_INTERVAL_MS, value * multiplier);
|
|
32
|
+
const prompt = match[3].trim();
|
|
33
|
+
if (!prompt) {
|
|
34
|
+
return { action: "start", error: "Usage: /loop <interval> <prompt> — the prompt is required." };
|
|
35
|
+
}
|
|
36
|
+
return { action: "start", intervalMs, prompt };
|
|
37
|
+
}
|
|
38
|
+
export function decideLoopFiring(loop, now, turnRunning) {
|
|
39
|
+
if (now < loop.nextFireAt)
|
|
40
|
+
return "wait";
|
|
41
|
+
return turnRunning ? "defer" : "fire";
|
|
42
|
+
}
|
|
43
|
+
export function formatLoopList(loops, now) {
|
|
44
|
+
if (loops.length === 0)
|
|
45
|
+
return "No active loops. Start one with /loop <interval> <prompt>.";
|
|
46
|
+
return loops
|
|
47
|
+
.map((loop) => {
|
|
48
|
+
const nextIn = Math.max(0, Math.round((loop.nextFireAt - now) / 1000));
|
|
49
|
+
return `#${loop.id} every ${formatInterval(loop.intervalMs)} · fired ${loop.fires}x · next in ${nextIn}s · ${truncate(loop.prompt, 60)}`;
|
|
50
|
+
})
|
|
51
|
+
.join("\n");
|
|
52
|
+
}
|
|
53
|
+
export function formatInterval(intervalMs) {
|
|
54
|
+
if (intervalMs % 3_600_000 === 0)
|
|
55
|
+
return `${intervalMs / 3_600_000}h`;
|
|
56
|
+
if (intervalMs % 60_000 === 0)
|
|
57
|
+
return `${intervalMs / 60_000}m`;
|
|
58
|
+
return `${Math.round(intervalMs / 1000)}s`;
|
|
59
|
+
}
|
|
60
|
+
function truncate(value, max) {
|
|
61
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
62
|
+
return oneLine.length <= max ? oneLine : `${oneLine.slice(0, max - 1)}…`;
|
|
63
|
+
}
|