brl-subagent 2.3.4 → 2.3.6

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 ADDED
@@ -0,0 +1,58 @@
1
+ # AGENT.md — brl-subagent capability reference
2
+
3
+ > The `delegate_task` prompt guidelines are a derived summary of this file; if they disagree, this file wins.
4
+
5
+ This file is the authoritative reference for how the brl-subagent extension behaves
6
+ and how you (the conductor) should judge a delegation. Read it when planning
7
+ delegation-heavy work.
8
+
9
+ ## Execution models
10
+
11
+ | Mode | What blocks | What wakes you | Batchable? |
12
+ | --- | --- | --- | --- |
13
+ | single, foreground | blocks until it returns | — (result comes back inline) | no |
14
+ | single, background | returns immediately with an agent id | yes — a `subagent-completion` message on terminal state | no |
15
+ | chain | blocks until the chain finishes or fails | — | yes (foreground) |
16
+ | parallel (`tasks`) | blocks until every task finishes | — | yes (foreground) |
17
+ | graph | blocks; runs in waves via `dependsOn` | — | yes (foreground) |
18
+
19
+ - Foreground single blocks: you wait for the result inline.
20
+ - Background single returns immediately with an agent id; a completion message wakes you (see below).
21
+ - Chain, parallel, and graph are batch — one call, many subtasks. They are foreground today; background batch is future work.
22
+ - Chain stops at the first failure; max 10 steps.
23
+ - Parallel runs every task regardless of the others — no short-circuit.
24
+ - Graph runs in waves, ordered by `dependsOn` edges.
25
+
26
+ ## The completion contract
27
+
28
+ - A background run wakes you with a structured `subagent-completion` message when it reaches a terminal state (`completed`, `failed`, or `stopped`). The `details` carry `id`, `status`, `errorCategory` (with `errorMessage` when present), cost/tokens/duration, and `label`; the content carries a tail of output.
29
+ - The wake triggers a turn even when you are idle. Do not poll for it.
30
+ - Delivery mode by status holds at knob `"all"`: `failed`/`stopped` → steering you to act; `completed` → follow-up. The `completionNotify` knob changes both the delivery mode and the wake: under `"failed"` a `completed` run is delivered as a passive `nextTurn` (no wake) while `failed`/`stopped` still steer; under `"off"` everything is a passive `nextTurn` and nothing wakes you. (The knob controls whether an idle conductor is triggered — see below.)
31
+ - Knob dependency: polling is correct only when wakes are disabled (`completionNotify: "off"`). The wake is per-terminal-status and knob-scoped: under `"all"` a terminal run always wakes you; under `"failed"` only a `failed` or `stopped` run wakes you — a `completed` run stays a passive `nextTurn` (no wake); under `"off"` nothing wakes you. So polling is wrong unless wakes are disabled. One status check as a stall check is legitimate; repeated polling is not.
32
+ - One message per run — the extension deduplicates on the first terminal event for a run id, so don't expect multiple messages for a single run.
33
+ - Honest records: trust the message's `category`/abort source over your own guess. The provider/abort origin is authoritative on why the run stopped.
34
+ - Steering is not an abort. Being steered to act on a terminal run is not terminating it. A real `stop` is an abort — and a stopped run still wakes you.
35
+
36
+ ## Delegation judgment
37
+
38
+ - Foreground vs background: if your next step needs the result inline, run foreground. If the user can keep working or is AFK, run background and act on the wake.
39
+ - Batch vs sequential: if the work is pre-declared, use chain/parallel/graph. If you're steering incrementally with the user, issue sequential calls.
40
+ - Retry taxonomy: `retryRunId` is a retry — it re-runs with the recorded task and params. A re-dispatch (a fresh call with the same task text) is not a retry. Never re-issue an identical spec after a termination without confirming the cause with the user (Rule 18).
41
+ - When to delegate: when the task needs an isolated context, a deep investigation, parallel research, or a long-running analysis.
42
+ - Approval: `approvalMode: "always"` is rejected in background — there is no dialog. Use `auto` (default) or `writes`; `writes` auto-approves in background.
43
+
44
+ ## Canonical shapes
45
+
46
+ - Batch chain with `{previous}`: `chain: [{ task: "…" }, { task: "Use {previous} …" }]` — each step gets the prior step's output.
47
+ - Graph with `dependsOn`: subtasks declare edges; the scheduler runs them in waves.
48
+ - Background + act on wake: spawn a background run, then act when the completion message arrives. Never poll between spawn and wake.
49
+ - retryRunId: pass a failed run's id to re-run it with its recorded task and params; explicit parameters override the originals.
50
+
51
+ ## Mechanics pointer
52
+
53
+ Presets and templates are documented in the schema, not here. Judgment only:
54
+
55
+ - Prefer a preset when the delegation matches a standard shape — review/audit/test/docs; override per-call deliberately.
56
+ - Recurring task shapes → a saved template.
57
+
58
+ The schema's `delegate_task` parameters, the `templateSummary` and `presetRestrictionSummary` lines in the prompt guidelines, and `get_subagent_result`'s description are the mechanics reference.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Multi-agent orchestration for [pi](https://github.com/earendil-works/pi) — chain, parallel, and dependency-graph delegation to isolated subagents with per-step model routing, preset-driven tool scoping, thinking-level control, and background execution with live monitoring, real abort, and per-agent timeouts.
4
4
 
5
- **Version:** 2.3.4 · **Author:** Azmeen Afandi / [Beeroo Labs](https://beeroolabs.com) · **License:** MIT
5
+ **Version:** 2.3.6 · **Author:** Azmeen Afandi / [Beeroo Labs](https://beeroolabs.com) · **License:** MIT
6
6
 
7
7
  ---
8
8
 
@@ -14,7 +14,7 @@
14
14
  - **True background execution** — live monitor, real abort (`stop_subagent`), per-agent timeouts, and hard caps. Nothing orphans; nothing leaks.
15
15
  - **Preset-driven tool scoping** — every subagent runs with exactly the tools its job needs, restricted by preset or per-call `tools`/`excludeTools`, with auto-route that picks the right preset when you don't.
16
16
  - **Templates with slots** — saved, file-backed task templates with `${param}` placeholders for workflows you run again and again.
17
- - **Safety by default** — task-fence injection protection, sanitized error paths, owner-only persistence, and a 700+ test suite pinning every contract against the real pi SDK.
17
+ - **Safety by default** — task-fence injection protection, sanitized error paths, owner-only persistence, and a 975+ test suite pinning every contract against the real pi SDK.
18
18
 
19
19
  ---
20
20
 
@@ -22,13 +22,18 @@
22
22
 
23
23
  ```bash
24
24
  # Global (all projects)
25
- cd ~/.pi/agent/extensions && git clone https://github.com/azmeenafandi/brl-subagent.git
25
+ pi install npm:brl-subagent
26
26
 
27
- # Project-local
28
- cd your-project/.pi/extensions && git clone https://github.com/azmeenafandi/brl-subagent.git
27
+ # Project-local (commit .pi/settings.json to share with your team)
28
+ pi install -l npm:brl-subagent
29
29
  ```
30
30
 
31
- Pi auto-discovers extensions in these directories. To update: `git pull` inside the cloned directory.
31
+ Pi installs the package and loads the extension automatically. Updates and removal:
32
+
33
+ ```bash
34
+ pi update --extensions # update installed packages
35
+ pi remove npm:brl-subagent # uninstall
36
+ ```
32
37
 
33
38
  ---
34
39
 
@@ -55,7 +60,6 @@ Pi auto-discovers extensions in these directories. To update: `git pull` inside
55
60
  | `/brl-subagent monitor` | Live monitor running subagents |
56
61
  | `/brl-subagent dashboard` | Live observability dashboard |
57
62
  | `/brl-subagent retry` | Browse failed runs to retry |
58
- | `/brl-subagent update-check` | Toggle update check on startup |
59
63
  | `/brl-subagent reset` | Reset all configuration |
60
64
 
61
65
  All settings persist across sessions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brl-subagent",
3
- "version": "2.3.4",
3
+ "version": "2.3.6",
4
4
  "description": "Subagent extension for pi with configurable model and thinking mode via selection UI",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -16,6 +16,7 @@
16
16
  "templates/",
17
17
  "README.md",
18
18
  "LICENSE",
19
+ "AGENT.md",
19
20
  "!src/__tests__/"
20
21
  ],
21
22
  "pi": {
@@ -41,7 +42,7 @@
41
42
  "jiti": "^2.7.0",
42
43
  "typebox": "^1.3.25",
43
44
  "typescript": "^5.9.3",
44
- "vitest": "^4.1.11"
45
+ "vitest": "^5.0.0"
45
46
  },
46
47
  "overrides": {
47
48
  "nanoid": "^3.3.17"
package/src/index.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  * See AGENT.md for the conductor-facing capability reference.
5
5
  */
6
6
 
7
- import * as fs from "node:fs";
8
7
  import * as path from "node:path";
9
8
  import type {
10
9
  AgentToolResult,
@@ -52,6 +51,7 @@ import {
52
51
  } from "./types";
53
52
  import { validateGraph, topologicalSort } from "./scheduler";
54
53
  import { resolveTemplate, loadAllTemplates, loadBuiltinTemplates, validateTemplatePresetRefs, extractParamNames } from "./templates";
54
+ import { pkgPath } from "./paths";
55
55
 
56
56
  import { sanitizeTask, validateCwd, validateOutputFile, stripAnsi, capOutput, getCurrentDepth, sanitizeErrorMessage, buildCrashResult } from "./sanitize";
57
57
  import {
@@ -92,7 +92,6 @@ import {
92
92
  showCompletionNotifySelector,
93
93
  showPresetManager,
94
94
  showTemplateManager,
95
- showUpdateCheckToggle,
96
95
  showSLAConfig,
97
96
  showSLAStats,
98
97
  showConfigMenu,
@@ -105,8 +104,6 @@ import {
105
104
  } from "./tui";
106
105
  import { createLogger, type Logger } from "./logging";
107
106
  import { Intercom } from "./messaging";
108
- import { checkForUpdates } from "./update";
109
- import { UPDATE_CHECK_INTERVAL_MS } from "./types";
110
107
  import * as eventBus from "./event-bus";
111
108
  import {
112
109
  buildCompletionMessage,
@@ -197,16 +194,6 @@ export function buildTemplateGuideline(templateSummary: string): string {
197
194
  export default function (pi: ExtensionAPI) {
198
195
  const log = createLogger("brl-subagent");
199
196
 
200
- // Read current version from package.json
201
- const currentVersion = (() => {
202
- try {
203
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8"));
204
- return pkg.version || "0.0.0";
205
- } catch {
206
- return "0.0.0";
207
- }
208
- })();
209
-
210
197
  // F7: Session-bound state — initialized per session
211
198
  let state = createSessionState(log);
212
199
 
@@ -1931,7 +1918,6 @@ export default function (pi: ExtensionAPI) {
1931
1918
  preset: () => showPresetManager(ctx, state),
1932
1919
  templates: () => showTemplateManager(ctx, state),
1933
1920
  retry: () => showRetryMenu(ctx, state),
1934
- "update-check": () => showUpdateCheckToggle(ctx, state, applyConfig),
1935
1921
  sla: () => showSLAConfig(ctx, state, applyConfig),
1936
1922
  "sla-stats": () => showSLAStats(ctx, state),
1937
1923
  };
@@ -1954,7 +1940,7 @@ export default function (pi: ExtensionAPI) {
1954
1940
 
1955
1941
  // B1: Load built-in presets once at registration to expose their tool
1956
1942
  // restrictions to the conductor before any delegate_task call happens.
1957
- const registrationPresets = loadBuiltinPresets(path.join(__dirname, "..", "presets"));
1943
+ const registrationPresets = loadBuiltinPresets(pkgPath("presets"));
1958
1944
  const presetRestrictionSummary = buildInventorySummary(registrationPresets, (p) => {
1959
1945
  const restricted = p.excludeTools?.length || p.tools?.length;
1960
1946
  if (!restricted) return p.name;
@@ -1965,9 +1951,14 @@ export default function (pi: ExtensionAPI) {
1965
1951
  // inventory (name + purpose + ${param} slots) to the conductor before any
1966
1952
  // delegate_task call. Custom/user templates can't be enumerated statically,
1967
1953
  // so the summary covers built-ins only and points at /brl-subagent templates.
1968
- const registrationTemplates = loadBuiltinTemplates(path.join(__dirname, "..", "templates"));
1954
+ const registrationTemplates = loadBuiltinTemplates(pkgPath("templates"));
1969
1955
  const templateSummary = buildInventorySummary(registrationTemplates, formatTemplateSummaryItem);
1970
1956
 
1957
+ // Issue #158: resolve the shipped AGENT.md from the extension's own directory
1958
+ // rather than a hardcoded sync-copy destination, so npm-installed packages
1959
+ // (installed via `pi install npm:brl-subagent`) can find it.
1960
+ const agentMdPath = pkgPath("AGENT.md");
1961
+
1971
1962
  pi.registerTool({
1972
1963
  name: "delegate_task",
1973
1964
  label: "Delegate Task",
@@ -1983,7 +1974,7 @@ export default function (pi: ExtensionAPI) {
1983
1974
  "Delegate tasks to a subagent for isolated, parallel or background work",
1984
1975
  promptGuidelines: [
1985
1976
  "Use delegate_task when the user asks you to hand off work to a subagent, or when a task would benefit from an isolated context window (e.g., deep investigation, parallel research, long-running analysis).",
1986
- "The authoritative capability reference for delegation is the extension's AGENT.md — read it at ~/.pi/agent/extensions/brl-subagent/AGENT.md, or ./AGENT.md in the project root when present, when planning delegation-heavy work.",
1977
+ `The authoritative capability reference for delegation is the extension's AGENT.md — read it at ${agentMdPath}, or ./AGENT.md in the project root when present, when planning delegation-heavy work.`,
1987
1978
  "The subagent inherits your system prompt and runs with its own model (configurable via /brl-subagent). It reports what it did when done.",
1988
1979
  "You can customize per-call via inheritSystemPrompt and systemPrompt: set inheritSystemPrompt: false to save context, provide a systemPrompt for custom instructions, or use both to add instructions on top of inheritance.",
1989
1980
  "Set thinkingLevel per call to match task complexity. The level is capped at the user's configured maximum. Map tasks to levels using this heuristic: off = file listing, grep, simple read. minimal = file diff, syntax check, find-and-replace. low = refactoring, test generation, documentation. medium = default — code review, debugging, moderate analysis. high = security audit, architecture review, complex debugging. xhigh = multi-step causal reasoning, research, novel problem solving. Default to 'off' or 'minimal' for trivial tasks — do not waste the user's budget.",
@@ -3691,11 +3682,11 @@ export default function (pi: ExtensionAPI) {
3691
3682
  // Issue #147: capture the session context for the completion-push subscriber.
3692
3683
  sessionCtx = ctx;
3693
3684
  // Load built-in presets
3694
- const presetsDir = path.join(__dirname, "..", "presets");
3685
+ const presetsDir = pkgPath("presets");
3695
3686
  state.builtinPresets = loadBuiltinPresets(presetsDir, log);
3696
3687
  state.customPresets = loadCustomPresets(ctx.cwd, log);
3697
3688
  // Builtin templates + merged full stack (custom overrides builtin)
3698
- const templatesDir = path.join(__dirname, "..", "templates");
3689
+ const templatesDir = pkgPath("templates");
3699
3690
  state.builtinTemplates = loadBuiltinTemplates(templatesDir, log);
3700
3691
  state.config.templates = loadAllTemplates(ctx.cwd, log, templatesDir);
3701
3692
 
@@ -3734,23 +3725,6 @@ export default function (pi: ExtensionAPI) {
3734
3725
  // F5/F9: Safe state restoration with type guards
3735
3726
  state.restoreFromSession(ctx);
3736
3727
 
3737
- // Check for updates (non-blocking, once per 24h)
3738
- if (state.config.updateCheckEnabled) {
3739
- const now = Date.now();
3740
- if (now - state.config.lastUpdateCheck > UPDATE_CHECK_INTERVAL_MS) {
3741
- state.config.lastUpdateCheck = now;
3742
- checkForUpdates(currentVersion, log).then((result) => {
3743
- if (result?.available) {
3744
- ctx.ui.notify(
3745
- "brl-subagent " + result.version + " available (current: " + currentVersion + "). Visit " + result.url + " to update. /brl-subagent update-check to disable.",
3746
- "info"
3747
- );
3748
- log.info("Update available", { current: currentVersion, latest: result.version });
3749
- }
3750
- }).catch(() => {}); // silently ignore errors
3751
- }
3752
- }
3753
-
3754
3728
  updateStatus(state, ctx);
3755
3729
  });
3756
3730
 
package/src/paths.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * brl-subagent — Package-root path resolution
3
+ *
4
+ * The extension is loaded from `<pkg>/src/` (package.json `pi.extensions`
5
+ * points at `./src/index.ts`), so every module in this package lives directly
6
+ * in `src/` and the package root is exactly one level up. `pkgPath()` is the
7
+ * single helper for resolving bundled assets against that root — the same base
8
+ * used for `presets/`, `templates/`, `package.json`, and `AGENT.md`.
9
+ */
10
+
11
+ import * as path from "node:path";
12
+
13
+ /**
14
+ * Resolve `segments` against the package root.
15
+ *
16
+ * Because the extension loads from `<pkg>/src/`, the package root is one level
17
+ * up from this module's directory. A wrong depth here would resolve inside
18
+ * `src/` and fail to find the bundled asset.
19
+ *
20
+ * @param segments - Path segments below the package root (e.g. `"presets"`, `"AGENT.md"`)
21
+ * @returns The absolute path to the bundled asset
22
+ */
23
+ export function pkgPath(...segments: string[]): string {
24
+ return path.join(__dirname, "..", ...segments);
25
+ }
package/src/state.ts CHANGED
@@ -142,8 +142,6 @@ export class SessionState {
142
142
  circuitBreaker: this.defaultCircuitBreaker(),
143
143
  slaTrackingEnabled: false,
144
144
  slaWindowSize: 50,
145
- updateCheckEnabled: true,
146
- lastUpdateCheck: 0,
147
145
  };
148
146
  }
149
147
 
@@ -168,8 +166,6 @@ export class SessionState {
168
166
  slaTrackingEnabled: this.config.slaTrackingEnabled,
169
167
  slaWindowSize: this.config.slaWindowSize,
170
168
  lastSLAMetrics: this.config.lastSLAMetrics,
171
- updateCheckEnabled: this.config.updateCheckEnabled,
172
- lastUpdateCheck: this.config.lastUpdateCheck,
173
169
  });
174
170
  }
175
171
 
@@ -258,10 +254,6 @@ export class SessionState {
258
254
  this.config.lastSLAMetrics = data.lastSLAMetrics as import("./types").SLAMetrics;
259
255
  }
260
256
 
261
- // Restore update check fields
262
- if (typeof data.updateCheckEnabled === "boolean") this.config.updateCheckEnabled = data.updateCheckEnabled;
263
- if (typeof data.lastUpdateCheck === "number" && data.lastUpdateCheck >= 0) this.config.lastUpdateCheck = data.lastUpdateCheck;
264
-
265
257
  this.log?.info("State restored from session", {
266
258
  model: data.model ? `${data.model.provider}/${data.model.id}` : "none",
267
259
  thinkingLevel: data.maxThinkingLevel,
@@ -508,8 +500,6 @@ export class SessionState {
508
500
  this.config.slaTrackingEnabled = false;
509
501
  this.config.slaWindowSize = 50;
510
502
  this.config.lastSLAMetrics = undefined;
511
- this.config.updateCheckEnabled = true;
512
- this.config.lastUpdateCheck = 0;
513
503
  }
514
504
  }
515
505
 
package/src/templates.ts CHANGED
@@ -27,6 +27,7 @@ import type { TaskTemplate, ThinkingLevel, SubagentPreset } from "./types";
27
27
  import { TEMPLATE_PARAM_RE, THINKING_LEVELS } from "./types";
28
28
  import type { Logger } from "./logging";
29
29
  import { parseFrontmatter, sanitizeFileName } from "./presets";
30
+ import { pkgPath } from "./paths";
30
31
 
31
32
  // ---------------------------------------------------------------------------
32
33
  // Param extraction
@@ -373,7 +374,7 @@ export function getAllTemplates(
373
374
  * the extension's presets/ dir.
374
375
  */
375
376
  export function loadAllTemplates(cwd: string, log?: Logger, templatesDir?: string): TaskTemplate[] {
376
- const builtinDir = templatesDir ?? path.join(__dirname, "..", "templates");
377
+ const builtinDir = templatesDir ?? pkgPath("templates");
377
378
  return getAllTemplates(loadBuiltinTemplates(builtinDir, log), loadCustomTemplates(cwd, log));
378
379
  }
379
380
 
package/src/tui.ts CHANGED
@@ -337,28 +337,6 @@ export async function showCompletionNotifySelector(
337
337
  onConfigChanged(ctx, `Completion notify mode set to ${result}`);
338
338
  }
339
339
 
340
- // ---------------------------------------------------------------------------
341
- // Update check toggle
342
- // ---------------------------------------------------------------------------
343
-
344
- export async function showUpdateCheckToggle(
345
- ctx: ExtensionContext,
346
- state: SessionState,
347
- onConfigChanged: (ctx: ExtensionContext, msg: string) => void,
348
- ): Promise<void> {
349
- const items: SelectItem[] = [
350
- { value: "enabled", label: "Enabled", description: "Check for updates on session start" },
351
- { value: "disabled", label: "Disabled", description: "Skip update check" },
352
- ];
353
-
354
- const result = await showSelectList(ctx, "Update Check", items, 5);
355
- if (!result) return;
356
-
357
- state.config.updateCheckEnabled = result === "enabled";
358
- onConfigChanged(ctx, `Update check ${result}`);
359
- }
360
-
361
- // ---------------------------------------------------------------------------
362
340
  // ---------------------------------------------------------------------------
363
341
  // E4: SLA tracking configuration
364
342
  // ---------------------------------------------------------------------------
@@ -1199,11 +1177,6 @@ export function getConfigMenuItems(state: SessionState): SelectItem[] {
1199
1177
  ? `Enabled (window: ${state.config.slaWindowSize})`
1200
1178
  : "Disabled",
1201
1179
  },
1202
- {
1203
- value: "update-check",
1204
- label: "Update Check",
1205
- description: state.config.updateCheckEnabled ? "Enabled" : "Disabled",
1206
- },
1207
1180
  ];
1208
1181
  }
1209
1182
 
package/src/types.ts CHANGED
@@ -209,8 +209,6 @@ export interface SubagentState {
209
209
  slaTrackingEnabled: boolean; // E4: SLA tracking toggle
210
210
  slaWindowSize: number; // E4: number of recent runs to analyze (10-500)
211
211
  lastSLAMetrics?: SLAMetrics; // E4: persisted baseline for degradation comparison
212
- updateCheckEnabled: boolean; // Version update notifier toggle
213
- lastUpdateCheck: number; // Epoch ms of last update check
214
212
  }
215
213
 
216
214
  /**
@@ -556,8 +554,6 @@ export const TEMPLATE_PARAM_RE = /\$\{(\w+)\}/g;
556
554
 
557
555
  export const DEFAULT_SESSION_COST_LIMIT = 0;
558
556
 
559
- export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
560
-
561
557
  export const CUSTOM_ENTRY_TYPES = {
562
558
  run: "brl-subagent-run",
563
559
  state: "brl-subagent-state",
@@ -589,7 +585,7 @@ export const RESERVED_NAME_PATTERN = /^__.*__$/;
589
585
  export const RESERVED_COMMAND_NAMES = new Set([
590
586
  "model", "thinking", "concurrency", "depth", "history", "monitor",
591
587
  "preset", "retry", "reset", "priority", "templates", "dashboard", "approval",
592
- "costlimit", "historyentries", "sla", "update-check", "completionnotify",
588
+ "costlimit", "historyentries", "sla", "completionnotify",
593
589
  "graph", "sla-stats",
594
590
  ]);
595
591
 
@@ -750,14 +746,6 @@ export function isSubagentStateShape(value: unknown): value is SubagentState {
750
746
  if (typeof v.slaWindowSize !== "number" || v.slaWindowSize < 10 || v.slaWindowSize > 500) return false;
751
747
  }
752
748
 
753
- // updateCheckEnabled must be a boolean if present
754
- if (v.updateCheckEnabled !== undefined && typeof v.updateCheckEnabled !== "boolean") return false;
755
-
756
- // lastUpdateCheck must be a non-negative number if present
757
- if (v.lastUpdateCheck !== undefined) {
758
- if (typeof v.lastUpdateCheck !== "number" || v.lastUpdateCheck < 0) return false;
759
- }
760
-
761
749
  return true;
762
750
  }
763
751
 
package/src/update.ts DELETED
@@ -1,109 +0,0 @@
1
- /**
2
- * brl-subagent — Version Update Notifier
3
- *
4
- * Checks GitHub releases for newer versions of brl-subagent.
5
- * Runs non-blocking with a 5-second timeout on session start.
6
- */
7
-
8
- import type { Logger } from "./logging";
9
-
10
- // ---------------------------------------------------------------------------
11
- // Types
12
- // ---------------------------------------------------------------------------
13
-
14
- export interface UpdateCheckResult {
15
- available: boolean;
16
- version?: string;
17
- url?: string;
18
- }
19
-
20
- // ---------------------------------------------------------------------------
21
- // Semver comparison
22
- // ---------------------------------------------------------------------------
23
-
24
- /**
25
- * Compare two semver strings (e.g., "2.1.0" vs "2.0.1").
26
- * Returns 1 if a > b, -1 if a < b, 0 if equal.
27
- */
28
- function compareSemver(a: string, b: string): number {
29
- const aParts = a.split(".").map(Number);
30
- const bParts = b.split(".").map(Number);
31
- const len = Math.max(aParts.length, bParts.length);
32
-
33
- for (let i = 0; i < len; i++) {
34
- const aVal = aParts[i] ?? 0;
35
- const bVal = bParts[i] ?? 0;
36
- if (aVal > bVal) return 1;
37
- if (aVal < bVal) return -1;
38
- }
39
- return 0;
40
- }
41
-
42
- // ---------------------------------------------------------------------------
43
- // Main export
44
- // ---------------------------------------------------------------------------
45
-
46
- /**
47
- * Check GitHub for a newer release of brl-subagent.
48
- *
49
- * @param currentVersion - The current version string (e.g., "2.0.1")
50
- * @param log - Optional logger for debug output
51
- * @returns Update check result, or null on any error (never throws)
52
- */
53
- export async function checkForUpdates(
54
- currentVersion: string,
55
- log?: Logger,
56
- ): Promise<UpdateCheckResult | null> {
57
- try {
58
- const controller = new AbortController();
59
- const timeoutId = setTimeout(() => controller.abort(), 5_000);
60
-
61
- const response = await fetch(
62
- "https://api.github.com/repos/azmeenafandi/brl-subagent/releases/latest",
63
- {
64
- headers: {
65
- Accept: "application/vnd.github+json",
66
- "User-Agent": "brl-subagent",
67
- },
68
- signal: controller.signal,
69
- },
70
- );
71
-
72
- clearTimeout(timeoutId);
73
-
74
- if (!response.ok) {
75
- log?.debug("Update check: non-OK response", { status: response.status });
76
- return null;
77
- }
78
-
79
- const data = (await response.json()) as {
80
- tag_name?: string;
81
- html_url?: string;
82
- };
83
-
84
- if (!data.tag_name || !data.html_url) {
85
- log?.debug("Update check: missing tag_name or html_url in response");
86
- return null;
87
- }
88
-
89
- // Strip "v" prefix from tag_name (e.g., "v2.1.0" → "2.1.0")
90
- const latestVersion = data.tag_name.replace(/^v/, "");
91
-
92
- const cmp = compareSemver(latestVersion, currentVersion);
93
-
94
- if (cmp > 0) {
95
- return {
96
- available: true,
97
- version: latestVersion,
98
- url: data.html_url,
99
- };
100
- }
101
-
102
- return { available: false };
103
- } catch (err) {
104
- log?.debug("Update check failed", {
105
- error: err instanceof Error ? err.message : String(err),
106
- });
107
- return null;
108
- }
109
- }