brl-subagent 2.3.9 → 2.4.0
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/AGENT.md +5 -0
- package/README.md +20 -8
- package/package.json +11 -9
- package/src/concurrency.ts +1 -0
- package/src/diff.ts +1 -0
- package/src/event-bus.ts +1 -0
- package/src/git.ts +1 -0
- package/src/history.ts +1 -0
- package/src/index.ts +60 -374
- package/src/logging.ts +21 -1
- package/src/messaging.ts +1 -0
- package/src/metrics.ts +1 -0
- package/src/model-availability.ts +1 -0
- package/src/notify-completion.ts +1 -0
- package/src/params.ts +1 -0
- package/src/paths.ts +1 -0
- package/src/preflight.ts +1 -0
- package/src/prelude.ts +1 -0
- package/src/presets.ts +1 -0
- package/src/prompt.ts +1 -0
- package/src/reports.ts +64 -8
- package/src/router.ts +1 -0
- package/src/runner.ts +1 -0
- package/src/sanitize.ts +1 -0
- package/src/scheduler.ts +1 -0
- package/src/schema.ts +254 -0
- package/src/session-manager.ts +44 -96
- package/src/state.ts +1 -0
- package/src/templates.ts +1 -0
- package/src/transcript-tail.ts +1 -0
- package/src/transcript.ts +1 -0
- package/src/tui-format.ts +1 -0
- package/src/tui.ts +3 -208
- package/src/types.ts +23 -2
- package/src/unit-run.ts +1 -0
- package/src/validate.ts +16 -2
package/src/tui-format.ts
CHANGED
package/src/tui.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Purpose: All TUI surfaces: config menus, preset/template managers, history, live monitor, and result rendering.
|
|
1
2
|
/**
|
|
2
3
|
* brl-subagent — TUI Components
|
|
3
4
|
*
|
|
@@ -17,7 +18,6 @@ import { Container, type SelectItem, SelectList, Spacer, Text, Markdown } from "
|
|
|
17
18
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
18
19
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
19
20
|
import type {
|
|
20
|
-
SubagentPreset,
|
|
21
21
|
TaskTemplate,
|
|
22
22
|
SubagentResult,
|
|
23
23
|
SubagentRun,
|
|
@@ -28,7 +28,6 @@ import type {
|
|
|
28
28
|
GraphDetails,
|
|
29
29
|
DelegateTaskDetails,
|
|
30
30
|
GraphTask,
|
|
31
|
-
SubTaskResult,
|
|
32
31
|
ThinkingLevel,
|
|
33
32
|
UsageStats,
|
|
34
33
|
FileDiff,
|
|
@@ -42,8 +41,6 @@ import {
|
|
|
42
41
|
COLLAPSED_OUTPUT_LINES,
|
|
43
42
|
COLLAPSED_DIFF_FILES_PREVIEW,
|
|
44
43
|
EXPANDED_HUNKS_PER_FILE,
|
|
45
|
-
RESERVED_NAME_PATTERN,
|
|
46
|
-
RESERVED_COMMAND_NAMES,
|
|
47
44
|
formatTokens,
|
|
48
45
|
formatUsageStats,
|
|
49
46
|
formatModel,
|
|
@@ -54,9 +51,9 @@ import {
|
|
|
54
51
|
isGraphDetails,
|
|
55
52
|
} from "./types";
|
|
56
53
|
import { buildFileAccessReport, buildSecretsExposureReport, generateComplianceSummary } from "./reports";
|
|
57
|
-
import { extractParamNames
|
|
54
|
+
import { extractParamNames } from "./templates";
|
|
58
55
|
import { parseDiff } from "./diff";
|
|
59
|
-
import { formatPresetSummary, getPreset,
|
|
56
|
+
import { formatPresetSummary, getPreset, loadCustomPresets } from "./presets";
|
|
60
57
|
import { formatRunDuration } from "./history";
|
|
61
58
|
import * as fs from "node:fs";
|
|
62
59
|
import * as path from "node:path";
|
|
@@ -638,185 +635,6 @@ export async function showComplianceMenu(
|
|
|
638
635
|
|
|
639
636
|
// Preset management UI
|
|
640
637
|
|
|
641
|
-
export async function showAddPreset(
|
|
642
|
-
ctx: ExtensionContext,
|
|
643
|
-
state: SessionState,
|
|
644
|
-
): Promise<void> {
|
|
645
|
-
// 1. Prompt for name
|
|
646
|
-
const name = await ctx.ui.input("Preset name (e.g., read-only-audit):");
|
|
647
|
-
if (!name?.trim()) return;
|
|
648
|
-
const trimmedName = name.trim();
|
|
649
|
-
|
|
650
|
-
// 2. Validate name (reserved patterns, existing presets)
|
|
651
|
-
if (RESERVED_NAME_PATTERN.test(trimmedName)) {
|
|
652
|
-
ctx.ui.notify("Names starting and ending with '__' are reserved.", "error");
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
if (RESERVED_COMMAND_NAMES.has(trimmedName)) {
|
|
656
|
-
ctx.ui.notify("Name '" + trimmedName + "' is reserved for a command.", "error");
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
if (getPreset(trimmedName, state.builtinPresets, state.customPresets)) {
|
|
660
|
-
ctx.ui.notify(`Preset "${trimmedName}" already exists.`, "error");
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// 3. Description
|
|
665
|
-
const description = await ctx.ui.input("Description (optional):");
|
|
666
|
-
|
|
667
|
-
// 4. Thinking level
|
|
668
|
-
const thinkingItems: SelectItem[] = [
|
|
669
|
-
{ value: "", label: "(not set — use conductor's choice)" },
|
|
670
|
-
...THINKING_LEVELS.map((level) => ({ value: level, label: level })),
|
|
671
|
-
];
|
|
672
|
-
const thinkingResult = await showSelectList(ctx, "Default Thinking Level", thinkingItems, 8);
|
|
673
|
-
|
|
674
|
-
// 5. Tool scope
|
|
675
|
-
const scopeItems: SelectItem[] = [
|
|
676
|
-
{ value: "all", label: "All tools (default)" },
|
|
677
|
-
{ value: "readonly", label: "Read-only (read, grep, find, ls)" },
|
|
678
|
-
{ value: "custom", label: "Custom tool list..." },
|
|
679
|
-
];
|
|
680
|
-
const scopeResult = await showSelectList(ctx, "Tool Scope", scopeItems, 5);
|
|
681
|
-
|
|
682
|
-
let tools: string[] | undefined;
|
|
683
|
-
let excludeTools: string[] | undefined;
|
|
684
|
-
let noBuiltinTools: boolean | undefined;
|
|
685
|
-
|
|
686
|
-
if (scopeResult === "readonly") {
|
|
687
|
-
tools = ["read", "grep", "find", "ls"];
|
|
688
|
-
excludeTools = ["write", "edit", "bash"];
|
|
689
|
-
} else if (scopeResult === "custom") {
|
|
690
|
-
const toolsStr = await ctx.ui.input("Tools (comma-separated):");
|
|
691
|
-
if (toolsStr?.trim()) tools = toolsStr.split(",").map((t) => t.trim()).filter(Boolean);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// 6. Inheritance
|
|
695
|
-
const inheritItems: SelectItem[] = [
|
|
696
|
-
{ value: "true", label: "Inherit system prompt (default)" },
|
|
697
|
-
{ value: "false", label: "No inheritance (standalone)" },
|
|
698
|
-
];
|
|
699
|
-
const inheritResult = await showSelectList(ctx, "System Prompt Inheritance", inheritItems, 3);
|
|
700
|
-
|
|
701
|
-
// 7. System prompt body
|
|
702
|
-
const systemPrompt = await ctx.ui.input("System prompt (optional, multi-line not supported yet):");
|
|
703
|
-
|
|
704
|
-
// 8. Build preset object
|
|
705
|
-
const preset: SubagentPreset = {
|
|
706
|
-
name: trimmedName,
|
|
707
|
-
description: description?.trim() || undefined,
|
|
708
|
-
thinkingLevel: thinkingResult || undefined,
|
|
709
|
-
inheritSystemPrompt: inheritResult === "false" ? false : undefined,
|
|
710
|
-
tools,
|
|
711
|
-
excludeTools,
|
|
712
|
-
noBuiltinTools,
|
|
713
|
-
systemPrompt: systemPrompt?.trim() || undefined,
|
|
714
|
-
};
|
|
715
|
-
|
|
716
|
-
// 9. Ask where to save
|
|
717
|
-
const locationItems: SelectItem[] = [
|
|
718
|
-
{ value: "project", label: "Project (.pi/brl-subagent/presets/)" },
|
|
719
|
-
{ value: "global", label: "Global (~/.pi/agent/brl-subagent/presets/)" },
|
|
720
|
-
];
|
|
721
|
-
const location = await showSelectList(ctx, "Save Location", locationItems, 3);
|
|
722
|
-
if (!location) return;
|
|
723
|
-
|
|
724
|
-
const homedir = process.env.HOME || process.env.USERPROFILE || "";
|
|
725
|
-
const targetDir = location === "project"
|
|
726
|
-
? path.join(ctx.cwd, ".pi", "brl-subagent", "presets")
|
|
727
|
-
: path.join(homedir, ".pi", "agent", "brl-subagent", "presets");
|
|
728
|
-
|
|
729
|
-
// 10. Write file
|
|
730
|
-
try {
|
|
731
|
-
writePresetFile(preset, targetDir);
|
|
732
|
-
// 11. Refresh custom presets
|
|
733
|
-
state.customPresets = loadCustomPresets(ctx.cwd, state.log);
|
|
734
|
-
// Reload templates alongside presets — both are file-derived, and a
|
|
735
|
-
// preset add/remove may fix (or break) a template's `preset:` reference.
|
|
736
|
-
// Full stack: custom (project+global) merged over builtins.
|
|
737
|
-
state.config.templates = loadAllTemplates(ctx.cwd, state.log);
|
|
738
|
-
// Issue #81: re-check template `preset:` refs after the mutation — warn
|
|
739
|
-
// (never skip) for dangling references instead of preset-less runs.
|
|
740
|
-
validateTemplatePresetRefs(
|
|
741
|
-
state.config.templates,
|
|
742
|
-
getAllPresets(state.builtinPresets, state.customPresets),
|
|
743
|
-
state.log,
|
|
744
|
-
);
|
|
745
|
-
ctx.ui.notify(`Preset "${trimmedName}" saved to ${location === "project" ? "project" : "global"} directory`, "info");
|
|
746
|
-
} catch (err) {
|
|
747
|
-
ctx.ui.notify(`Failed to save preset: ${(err as Error).message}`, "error");
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
export async function showRemovePreset(
|
|
752
|
-
ctx: ExtensionContext,
|
|
753
|
-
state: SessionState,
|
|
754
|
-
): Promise<void> {
|
|
755
|
-
const homedir = process.env.HOME || process.env.USERPROFILE || "";
|
|
756
|
-
const projectDir = path.join(ctx.cwd, ".pi", "brl-subagent", "presets");
|
|
757
|
-
const globalDir = path.join(homedir, ".pi", "agent", "brl-subagent", "presets");
|
|
758
|
-
|
|
759
|
-
// Scan both directories for .md files
|
|
760
|
-
const entries: Array<{ name: string; source: "project" | "global"; filePath: string }> = [];
|
|
761
|
-
|
|
762
|
-
for (const dir of [projectDir, globalDir]) {
|
|
763
|
-
const source = dir === projectDir ? "project" : "global";
|
|
764
|
-
try {
|
|
765
|
-
const files = fs.readdirSync(dir);
|
|
766
|
-
for (const file of files) {
|
|
767
|
-
if (!file.endsWith(".md")) continue;
|
|
768
|
-
const filePath = path.join(dir, file);
|
|
769
|
-
try {
|
|
770
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
771
|
-
const { meta } = parseFrontmatter(content);
|
|
772
|
-
if (meta.name && typeof meta.name === "string") {
|
|
773
|
-
entries.push({ name: meta.name as string, source, filePath });
|
|
774
|
-
}
|
|
775
|
-
} catch {
|
|
776
|
-
// Skip unparseable files
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
} catch {
|
|
780
|
-
// Directory doesn't exist — fine
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
if (entries.length === 0) {
|
|
785
|
-
ctx.ui.notify("No custom presets to remove.", "info");
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// Show list with source indicators
|
|
790
|
-
const items: SelectItem[] = entries.map((e) => ({
|
|
791
|
-
value: e.filePath,
|
|
792
|
-
label: `[${e.source === "project" ? "P" : "G"}] ${e.name}`,
|
|
793
|
-
description: e.source === "project" ? "Project-local" : "Global",
|
|
794
|
-
}));
|
|
795
|
-
|
|
796
|
-
const result = await showSelectList(ctx, "Remove Preset", items, 10);
|
|
797
|
-
if (!result) return;
|
|
798
|
-
|
|
799
|
-
// Delete the file
|
|
800
|
-
try {
|
|
801
|
-
fs.unlinkSync(result);
|
|
802
|
-
state.customPresets = loadCustomPresets(ctx.cwd, state.log);
|
|
803
|
-
// Reload templates too — a removed preset may orphan a template's
|
|
804
|
-
// `preset:` reference, so both file-derived collections refresh together.
|
|
805
|
-
// Full stack: custom (project+global) merged over builtins.
|
|
806
|
-
state.config.templates = loadAllTemplates(ctx.cwd, state.log);
|
|
807
|
-
// Issue #81: re-check template `preset:` refs after the mutation — warn
|
|
808
|
-
// (never skip) for dangling references instead of preset-less runs.
|
|
809
|
-
validateTemplatePresetRefs(
|
|
810
|
-
state.config.templates,
|
|
811
|
-
getAllPresets(state.builtinPresets, state.customPresets),
|
|
812
|
-
state.log,
|
|
813
|
-
);
|
|
814
|
-
ctx.ui.notify("Preset removed", "info");
|
|
815
|
-
} catch (err) {
|
|
816
|
-
ctx.ui.notify(`Failed to remove preset: ${(err as Error).message}`, "error");
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
638
|
export async function showPresetManager(
|
|
821
639
|
ctx: ExtensionContext,
|
|
822
640
|
state: SessionState,
|
|
@@ -2179,29 +1997,6 @@ function renderCollapsedText(
|
|
|
2179
1997
|
return text;
|
|
2180
1998
|
}
|
|
2181
1999
|
|
|
2182
|
-
// ---------------------------------------------------------------------------
|
|
2183
|
-
// Chain / parallel helper: one-line subtask summary
|
|
2184
|
-
// ---------------------------------------------------------------------------
|
|
2185
|
-
|
|
2186
|
-
function renderSubTaskSummary(
|
|
2187
|
-
result: SubTaskResult,
|
|
2188
|
-
theme: {
|
|
2189
|
-
fg: (color: string, text: string) => string;
|
|
2190
|
-
},
|
|
2191
|
-
maxLines: number,
|
|
2192
|
-
): string {
|
|
2193
|
-
const isError = isSubagentError(result);
|
|
2194
|
-
const icon = isError
|
|
2195
|
-
? theme.fg("error", "\u2717")
|
|
2196
|
-
: theme.fg("success", "\u2713");
|
|
2197
|
-
const label = result.label
|
|
2198
|
-
? theme.fg("accent", `${result.label}: `)
|
|
2199
|
-
: "";
|
|
2200
|
-
const output = getFinalOutput(result.messages);
|
|
2201
|
-
const preview = output.split("\n").slice(0, maxLines).join("\n");
|
|
2202
|
-
return `${icon} ${label}${theme.fg("toolOutput", preview)}`;
|
|
2203
|
-
}
|
|
2204
|
-
|
|
2205
2000
|
// ---------------------------------------------------------------------------
|
|
2206
2001
|
// Chain / parallel collapsed rendering
|
|
2207
2002
|
// ---------------------------------------------------------------------------
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Purpose: Shared types, constants, and the error/termination classification taxonomy.
|
|
1
2
|
/**
|
|
2
3
|
* brl-subagent — Type Definitions
|
|
3
4
|
*
|
|
@@ -6,6 +7,8 @@
|
|
|
6
7
|
|
|
7
8
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
9
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
10
|
+
import type { Static } from "typebox";
|
|
11
|
+
import type { delegateTaskParamsSchema } from "./schema";
|
|
9
12
|
import type { TranscriptMessage } from "./transcript-tail";
|
|
10
13
|
|
|
11
14
|
// ---------------------------------------------------------------------------
|
|
@@ -458,7 +461,7 @@ export interface GraphTask {
|
|
|
458
461
|
noBuiltinTools?: boolean;
|
|
459
462
|
systemPrompt?: string;
|
|
460
463
|
inheritSystemPrompt?: boolean;
|
|
461
|
-
dependsOn
|
|
464
|
+
dependsOn?: string[];
|
|
462
465
|
}
|
|
463
466
|
|
|
464
467
|
/** One wave of tasks executed together in a graph. */
|
|
@@ -492,6 +495,24 @@ export type DelegateTaskDetails =
|
|
|
492
495
|
| GraphDetails
|
|
493
496
|
| undefined;
|
|
494
497
|
|
|
498
|
+
/**
|
|
499
|
+
* The delegate_task handler's params (issue #229 item 4) — the type the
|
|
500
|
+
* handler's inline block used to spell out, hoisted here so it is nameable.
|
|
501
|
+
*
|
|
502
|
+
* Why it matters: `approvalMode` is in the registered schema and in
|
|
503
|
+
* KNOWN_DELEGATE_KEYS, and reaches execute() at runtime through the spread —
|
|
504
|
+
* but a reader of `params` inside the handler could not see it. The key-set
|
|
505
|
+
* is compile-time ratcheted in src/index.ts against KNOWN_DELEGATE_KEYS
|
|
506
|
+
* (schema ↔ KNOWN_DELEGATE_KEYS ↔ this type); the missing key is invisible at
|
|
507
|
+
* runtime, so only the compiler can catch its removal.
|
|
508
|
+
*
|
|
509
|
+
* A type ALIAS, not an interface: the handler passes `params` to
|
|
510
|
+
* findUnknownParams/resolveSubagentParams (Record<string, unknown> params),
|
|
511
|
+
* and only a type alias gets the implicit index signature an interface
|
|
512
|
+
* lacks.
|
|
513
|
+
*/
|
|
514
|
+
export type DelegateTaskParams = Static<typeof delegateTaskParamsSchema>;
|
|
515
|
+
|
|
495
516
|
// ---------------------------------------------------------------------------
|
|
496
517
|
// Resolved params (after preset merging + validation)
|
|
497
518
|
// ---------------------------------------------------------------------------
|
|
@@ -636,7 +657,7 @@ export const RESERVED_COMMAND_NAMES = new Set([
|
|
|
636
657
|
"model", "thinking", "concurrency", "depth", "history", "monitor",
|
|
637
658
|
"preset", "retry", "reset", "priority", "templates", "dashboard", "approval",
|
|
638
659
|
"costlimit", "historyentries", "sla", "completionnotify",
|
|
639
|
-
"graph", "sla-stats",
|
|
660
|
+
"graph", "sla-stats", "compliance",
|
|
640
661
|
]);
|
|
641
662
|
|
|
642
663
|
// ---------------------------------------------------------------------------
|
package/src/unit-run.ts
CHANGED
package/src/validate.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Purpose: H1 pre-task validation: deterministic tool/thinking/git checks and failure post-mortems.
|
|
1
2
|
/**
|
|
2
3
|
* H1: Pre-task validation
|
|
3
4
|
*
|
|
@@ -382,6 +383,17 @@ export function validatePreTask(config: ValidateConfig): ValidateResult {
|
|
|
382
383
|
};
|
|
383
384
|
}
|
|
384
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Default deadline for a BACKGROUND run that specifies no explicit `timeout`
|
|
388
|
+
* (issue #28 orphan protection). This is a DEFAULT, not a ceiling: an explicit
|
|
389
|
+
* timeout is honored verbatim (after normalizeTimeout), however long it is
|
|
390
|
+
* (issue #240). Callers must NOT treat this as an upper bound.
|
|
391
|
+
*
|
|
392
|
+
* Kept next to normalizeTimeout (same setTimeout-boundary concern) and shared
|
|
393
|
+
* by the session-manager W3 timer and the index.ts hard-cap timer.
|
|
394
|
+
*/
|
|
395
|
+
export const DEFAULT_BACKGROUND_DEADLINE_MS = 30 * 60 * 1000;
|
|
396
|
+
|
|
385
397
|
/**
|
|
386
398
|
* Normalize a user-supplied timeout to a safe deadline value, or undefined for
|
|
387
399
|
* "no timeout". Guards the setTimeout boundary — Node fires `setTimeout(fn, 0)`,
|
|
@@ -390,8 +402,10 @@ export function validatePreTask(config: ValidateConfig): ValidateResult {
|
|
|
390
402
|
* review M1/m1).
|
|
391
403
|
*
|
|
392
404
|
* Returns undefined for: undefined, 0, negative, NaN, Infinity, >=2^31-1.
|
|
393
|
-
* Returns the value unchanged otherwise
|
|
394
|
-
*
|
|
405
|
+
* Returns the value unchanged otherwise. A background run with no explicit
|
|
406
|
+
* timeout (undefined here) falls back to DEFAULT_BACKGROUND_DEADLINE_MS; an
|
|
407
|
+
* explicit timeout is honored verbatim — the 30m default is not a ceiling
|
|
408
|
+
* (issue #240).
|
|
395
409
|
*/
|
|
396
410
|
export function normalizeTimeout(timeout: number | undefined): number | undefined {
|
|
397
411
|
if (timeout === undefined) return undefined;
|