brl-subagent 2.3.7 → 2.3.9

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/src/types.ts CHANGED
@@ -299,6 +299,14 @@ export interface SubagentRun {
299
299
  excludeTools?: string[];
300
300
  noBuiltinTools?: boolean;
301
301
  preset?: string;
302
+ // Issue #227: the execution-shape fields survive retries, so a retried
303
+ // background run stays background and a retried branch-mode run keeps its
304
+ // work branch, its approvalMode gating and its force override. Additive:
305
+ // records written before this change simply lack the keys.
306
+ background?: boolean;
307
+ gitMode?: string;
308
+ approvalMode?: string;
309
+ force?: boolean;
302
310
  errorCategory?: ErrorCategory;
303
311
  };
304
312
  }
@@ -504,6 +512,9 @@ export interface ResolvedParams {
504
512
  toolOptions: SubagentToolOptions | undefined;
505
513
  /** Set only when autoRoutePreset chose the preset (not explicitly requested). */
506
514
  autoRoutedPreset: SubagentPreset | undefined;
515
+ /** Set only when auto-route chose the preset: the keyword that matched —
516
+ * surfaced in the dispatch result as the auto-route evidence. */
517
+ autoRouteKeyword?: string;
507
518
  }
508
519
 
509
520
  // ---------------------------------------------------------------------------
@@ -808,6 +819,27 @@ export function classifyTerminalOutcome(
808
819
  }
809
820
  }
810
821
 
822
+ /**
823
+ * Issue #179 (C1 fix): a failed run entry/result must carry a terminal reason
824
+ * that AGREES with its status — never undefined, never "stop". Any reason the
825
+ * classifier already treats as a failure (error / length / toolUse / deferred
826
+ * / pending / aborted) is coherent and kept; a missing or success reason is
827
+ * coerced to the honest failure reason derived from the classified category.
828
+ *
829
+ * Issue #187: lives here (not session-manager) so BOTH the background
830
+ * finalize (session-manager.ts) and the foreground finalize (history.ts) apply
831
+ * the SAME rule through one implementation instead of two copies.
832
+ */
833
+ export function coherentFailureReason(
834
+ stopReason: string | undefined,
835
+ errorCategory: ErrorCategory | undefined,
836
+ ): string {
837
+ if (stopReason && classifyTerminalOutcome(stopReason).status !== "completed") {
838
+ return stopReason;
839
+ }
840
+ return errorCategory === "aborted" ? "aborted" : "error";
841
+ }
842
+
811
843
  export function formatModel(m: { provider: string; id: string } | undefined): string {
812
844
  return m ? `${m.provider}/${m.id}` : "Not set (will use main agent\u2019s model)";
813
845
  }
package/src/unit-run.ts CHANGED
@@ -122,6 +122,10 @@ export function createUnitRun(
122
122
  // same priority the run entry itself carried.
123
123
  priority: source.priority ?? priorityFloor,
124
124
  startedAt: new Date().toISOString(),
125
+ // The execution-shape fields (background/gitMode/approvalMode/force) are
126
+ // deliberately omitted: a fan-out unit retries as a SINGLE run, so it must
127
+ // not inherit the fan-out's background-ness. A retry that wants background
128
+ // passes background: true explicitly.
125
129
  originalParams: snapshotOriginalParams({
126
130
  systemPrompt: source.customSP,
127
131
  inheritSystemPrompt: source.inheritSP,
package/src/validate.ts CHANGED
@@ -17,6 +17,15 @@ export interface ValidateConfig {
17
17
  thinkingLevel?: ThinkingLevel;
18
18
  gitMode?: string;
19
19
  outputFile?: string;
20
+ /**
21
+ * delegate_task's `force` param (default false). When true, capability
22
+ * ERRORS (the high-confidence capability-critical mismatches below)
23
+ * degrade to warning-class: they still surface in `warnings`, but
24
+ * `valid` stays true and dispatch proceeds. The outputFile hard
25
+ * conflict is NOT suppressible — a report the subagent cannot write
26
+ * can never succeed, so force does not apply to it.
27
+ */
28
+ force?: boolean;
20
29
  }
21
30
 
22
31
  export interface ValidateResult {
@@ -29,40 +38,109 @@ export interface ValidateResult {
29
38
 
30
39
  interface ToolRequirement {
31
40
  patterns: RegExp[];
41
+ /** ALL-of: every listed tool must be available (default semantics). */
32
42
  requiredTools: string[];
43
+ /**
44
+ * ANY-of: when set, ONE available tool satisfies the requirement and
45
+ * `requiredTools` is ignored — directory exploration is a single
46
+ * capability (the browse built-ins find/ls/grep and bash are
47
+ * interchangeable ways to do it), not a conjunction of all of them.
48
+ */
49
+ anyOfTools?: string[];
50
+ /**
51
+ * Warning lead, rendered as `Task ${description} but …` (and, under a
52
+ * unit prefix, `Task 1 ("deploy"): Task ${description} …`). Starts with
53
+ * the verb ("involves …") so the fixed `Task ` template reads naturally
54
+ * standalone AND prefixed — do not add a leading "task" (issue #220).
55
+ */
33
56
  description: string;
57
+ /**
58
+ * High-confidence, capability-critical subset of `patterns`: a task
59
+ * matching any of these with the capability ABSENT is promoted from
60
+ * warning to ERROR — dispatch does not spawn — unless `force` is set,
61
+ * which degrades the mismatch back to warning class. Only the two
62
+ * capability-critical shapes carry this: a run/execute/test/compile/
63
+ * benchmark task with no bash, and an exploration task with none of
64
+ * find/ls/grep/bash. Entry-less requirements never block (warnings only).
65
+ */
66
+ blockingPatterns?: RegExp[];
67
+ /**
68
+ * Builds the block message: names the missing capability, the resolved
69
+ * toolset, and the exact fix — including the force override. Receives
70
+ * the rendered `tools=…, excludeTools=…` toolset string.
71
+ */
72
+ blockMessage?: (toolset: string) => string;
34
73
  }
35
74
 
75
+ /*
76
+ * COVERAGE BOUNDARY — which wording evades this table (every guard states
77
+ * its coverage). These are word-boundary keyword matches against the raw
78
+ * task text, nothing more. EVADED by: paraphrases that use none of the
79
+ * listed keywords ("hunt for drafts", "browse the repository", "see which
80
+ * files mention X"), inflected forms the patterns do not list ("running",
81
+ * "tests", "searching", "listing" — \btest\b does not match "tests"), and
82
+ * domain phrasing that never names the verb ("kick off CI", "smoke the
83
+ * pipeline"). The opposite failure is equally real: documentation/writing
84
+ * tasks that merely MENTION a keyword ("document how to run the tests")
85
+ * match too — which is why only the two high-confidence capability blocks
86
+ * (blockingPatterns below) escalate to errors, while install/deploy/npm/
87
+ * yarn/pnpm/cargo/pip stay warning-class (docs can name them without
88
+ * needing them). Widening the table is deliberate work: add the keyword
89
+ * here AND update this note.
90
+ */
91
+
36
92
  const TOOL_REQUIREMENTS: ToolRequirement[] = [
37
93
  {
38
94
  patterns: [/\b(write|create|generate|build|implement|add|insert|append)\b/i],
39
95
  requiredTools: ['write', 'edit'],
40
- description: 'task involves writing/creating files',
96
+ description: 'involves writing/creating files',
41
97
  },
42
98
  {
43
99
  patterns: [/\b(edit|modify|update|change|fix|patch|refactor|rename)\b/i],
44
100
  requiredTools: ['write', 'edit'],
45
- description: 'task involves editing files',
101
+ description: 'involves editing files',
46
102
  },
47
103
  {
48
104
  patterns: [/\b(run|execute|test|benchmark|compile|install|deploy|npm|yarn|pnpm|cargo|pip|vitest)\b/i],
49
105
  requiredTools: ['bash'],
50
- description: 'task involves running commands',
106
+ description: 'involves running commands',
107
+ // Blocking subset: only the verbs that unambiguously DEMAND a shell.
108
+ // install/deploy/npm/yarn/pnpm/cargo/pip deliberately stay warning-class
109
+ // — documentation tasks can mention them without executing them.
110
+ blockingPatterns: [/\b(run|execute|test|compile|benchmark)\b/i],
111
+ blockMessage: (toolset) =>
112
+ `Cannot delegate: the task requires the 'bash' tool (running commands) but it is not available ` +
113
+ `in the resolved toolset (${toolset}). Fix: add 'bash' to this dispatch's tools (or remove it ` +
114
+ `from excludeTools) and re-dispatch, or pass force: true to dispatch anyway.`,
115
+ },
116
+ {
117
+ // Directory exploration is ONE capability, satisfied by ANY of the
118
+ // browse built-ins or bash — anyOfTools, not a conjunction.
119
+ patterns: [/\b(search|grep|find|list|locate|glob)\b/i],
120
+ requiredTools: [],
121
+ anyOfTools: ['find', 'ls', 'grep', 'bash'],
122
+ description: 'involves directory exploration',
123
+ blockingPatterns: [/\b(search|grep|find|list|locate|glob)\b/i],
124
+ blockMessage: (toolset) =>
125
+ `Cannot delegate: the task requires directory exploration, satisfied by any of 'find', 'ls', ` +
126
+ `'grep' or 'bash', but none is available in the resolved toolset (${toolset}). Fix: add any of ` +
127
+ `'find', 'ls', 'grep' or 'bash' to this dispatch's tools (or remove it from excludeTools) and ` +
128
+ `re-dispatch, or pass force: true to dispatch anyway.`,
51
129
  },
52
130
  {
53
131
  patterns: [/\b(delete|remove|clean|prune|uninstall|rm)\b/i],
54
132
  requiredTools: ['write', 'bash'],
55
- description: 'task involves deleting files',
133
+ description: 'involves deleting files',
56
134
  },
57
135
  {
58
136
  patterns: [/\b(commit|push|pull|merge|rebase|checkout|branch)\b/i],
59
137
  requiredTools: ['bash'],
60
- description: 'task involves git operations',
138
+ description: 'involves git operations',
61
139
  },
62
140
  {
63
141
  patterns: [/\b(audit|review|analyze|examine|check|inspect|scan|read)\b/i],
64
142
  requiredTools: ['read'],
65
- description: 'task involves reading/analyzing files',
143
+ description: 'involves reading/analyzing files',
66
144
  },
67
145
  ];
68
146
 
@@ -225,15 +303,38 @@ export function validatePreTask(config: ValidateConfig): ValidateResult {
225
303
  return { valid: true, warnings: [], errors: [] };
226
304
  }
227
305
 
228
- // Check tool requirements (warnings, not errors — conductor can override)
306
+ // Check tool requirements. A missing capability is warning-class by
307
+ // default (conductor can judge); the high-confidence capability-critical
308
+ // cases (blockingPatterns) are promoted to ERRORS unless `force` is set.
309
+ const toolset = `tools=${config.toolOptions?.tools?.join(',') ?? 'all'}, excludeTools=${config.toolOptions?.excludeTools?.join(',') ?? 'none'}`;
229
310
  for (const req of TOOL_REQUIREMENTS) {
230
311
  const matches = req.patterns.some(p => p.test(taskText));
231
312
  if (!matches) continue;
232
313
 
233
- for (const tool of req.requiredTools) {
234
- if (!isToolAvailable(tool, config.toolOptions)) {
314
+ const missing = req.anyOfTools
315
+ ? (req.anyOfTools.some(t => isToolAvailable(t, config.toolOptions)) ? [] : [...req.anyOfTools])
316
+ : req.requiredTools.filter(t => !isToolAvailable(t, config.toolOptions));
317
+ if (missing.length === 0) continue;
318
+
319
+ // Capability-critical block: high-confidence pattern matched AND the
320
+ // capability is absent. `force` degrades it to warning class below.
321
+ const capabilityBlock =
322
+ !config.force &&
323
+ req.blockMessage !== undefined &&
324
+ (req.blockingPatterns ?? []).some(p => p.test(taskText));
325
+ if (capabilityBlock) {
326
+ errors.push(req.blockMessage!(toolset));
327
+ continue;
328
+ }
329
+
330
+ if (req.anyOfTools) {
331
+ warnings.push(
332
+ `Task ${req.description} but none of ${req.anyOfTools.map(t => `'${t}'`).join(', ')} is available (${toolset})`,
333
+ );
334
+ } else {
335
+ for (const tool of missing) {
235
336
  warnings.push(
236
- `Task ${req.description} but '${tool}' is not available (tools=${config.toolOptions?.tools?.join(',') ?? 'all'}, excludeTools=${config.toolOptions?.excludeTools?.join(',') ?? 'none'})`,
337
+ `Task ${req.description} but '${tool}' is not available (${toolset})`,
237
338
  );
238
339
  }
239
340
  }
@@ -7,4 +7,4 @@ thinkingLevel: medium
7
7
  Review ${target} for correctness, security, and style.
8
8
 
9
9
  Report findings as a numbered list with file:line references,
10
- prioritized blocker → nit. Read-only — no edits.
10
+ prioritized blocker → nit. Read-only — no edits. Any finding that rests on a search or a read states its coverage boundary: the query or command used, its count or range, and how it reconciles with what you expected to cover.
@@ -6,4 +6,5 @@ thinkingLevel: medium
6
6
  ---
7
7
  Debug ${symptom}. Reproduce it first, then isolate the root cause.
8
8
 
9
- Show the failing input, the root cause, and the minimal fix.
9
+ Show the failing input, the root cause, and the minimal fix. Final report: end with a Coverage line — the exact command or query used, the count or range it printed, and how it reconciles with the corpus you expected; show an empty result's size before treating it as absence.
10
+ Before running any probe or reproduction, name its target by unique surrounding context and state the expected failure signature; when it runs tests, scope the run to the specific test file — a passing probe is first evidence the target was wrong, not that the behavior is fine.
@@ -7,4 +7,4 @@ thinkingLevel: medium
7
7
  Write tests for ${file} covering the happy path and edge cases.
8
8
 
9
9
  Include the test file path in your report and note any
10
- untestable code paths you skipped.
10
+ untestable code paths you skipped. When designing a mutation to prove a test fails on the defect, name the target by unique surrounding context and state the expected failure first.