Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "agentic-advisor",
"description": "Before writing code, grades how fragile the target is from LinearB health signals + local git history and holds the agent to a matching effort level (LOW/MEDIUM/HIGH) — lean on calm repos, defensive on fragile ones.",
"description": "Before writing code, grades how fragile the target is from LinearB health signals + local git history and reports whether it Reduced, Kept or Raised effort against the model's current level — lean on calm repos, defensive on fragile ones.",
"author": {
"name": "LinearB",
"email": "support@linearb.io"
Expand Down
4 changes: 2 additions & 2 deletions plugins/agentic-advisor/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "agentic-advisor",
"version": "1.4.7",
"description": "Before writing code, grades how fragile the target is from LinearB health signals + local git history (rework, incidents, fix/revert density, code ownership) and holds the agent to a matching effort level (LOW/MEDIUM/HIGH) — lean on calm repos, defensive on fragile ones. Auto-triggers via a bundled hook and can report each effort decision to your LinearB org as usage telemetry.",
"version": "1.5.0",
"description": "Before writing code, grades how fragile the target is from LinearB health signals + local git history (rework, incidents, fix/revert density, code ownership) and reports whether it Reduced, Kept or Raised effort against the model's current level — lean on calm repos, defensive on fragile ones. Auto-triggers via a bundled hook and can report each effort decision to your LinearB org as usage telemetry.",
"author": {
"name": "LinearB",
"email": "support@linearb.io"
Expand Down
7 changes: 7 additions & 0 deletions plugins/agentic-advisor/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [1.5.0] - 2026-11-04

### Changed

- The verdict now compares the grade with the model's current effort (`/effort`): `Raised effort (medium → high; …)`, `Kept effort (high; …)` or `Reduced effort (max → medium; …)`. It never raises past high, and a HIGH grade on xhigh or max is Kept.
- The decision event adds `effort_change` and `target_effort`; `effort_level` stays LOW/MEDIUM/HIGH, and the older `MEDIUM effort` lines are still reported.

## [1.4.7] - 2026-10-26

### Fixed
Expand Down
6 changes: 3 additions & 3 deletions plugins/agentic-advisor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Sizes the agent's coding **effort** to how fragile the target is — before it writes code.

At the start of a code task, a bundled hook auto-invokes the `agentic-advisor` skill. It reads the repo's current health (LinearB rework %, unreviewed merges, recent incidents) plus local git history on the files being touched, then grades **LOW / MEDIUM / HIGH effort** and holds the agent to it: lean and token-efficient on calm repos, defensive (smallest change, guards, tests, clarifying questions) on fragile ones. It never blocks; if LinearB data is unavailable it defaults to MEDIUM.
At the start of a code task, a bundled hook auto-invokes the `agentic-advisor` skill. It reads the repo's current health (LinearB rework %, unreviewed merges, recent incidents) plus local git history on the files being touched, then compares the grade with the model's current effort and says whether it **Reduced**, **Kept** or **Raised** effort, and holds the agent to it: lean and token-efficient on calm repos, defensive (smallest change, guards, tests, clarifying questions) on fragile ones. It never blocks; if LinearB data is unavailable it defaults to MEDIUM.

**Changelog:** [CHANGELOG.md](CHANGELOG.md)

Expand All @@ -19,9 +19,9 @@ Then add `LINEARB_API_TOKEN` to `~/.claude/settings.json` under `env` (see [Setu

The agent prints a one-line verdict before writing code, e.g.:

> LinearB: api-service — LOW effort (healthy: rework 0.4%, 0 unreviewed merges, no incidents).
> LinearB: api-service — Reduced effort (high → low; healthy: rework 0.4%, 0 unreviewed merges, no incidents).

> LinearB: payments-service — HIGH effort (rework 9.5% NEEDS FOCUS; target PaymentForm.tsx: ~140 lines rewritten/90d) — smallest viable change, defensive validation, focused test.
> LinearB: payments-service — Raised effort (medium → high; rework 9.5% NEEDS FOCUS; target PaymentForm.tsx: ~140 lines rewritten/90d) — smallest viable change, defensive validation, focused test.

## How it decides

Expand Down
36 changes: 28 additions & 8 deletions plugins/agentic-advisor/hooks/agentic-advisor-report.sh
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ if ! { [ -d "$marker_dir" ] && [ -O "$marker_dir" ] && [ -w "$marker_dir" ]; };
state="$marker_dir/${session_id:-default}.reported"
touch "$state" 2>/dev/null || true

# A graded verdict line: the Reduced/Kept/Raised shape, or the older LOW/MEDIUM/HIGH one.
GRADED_RE='LinearB: .+ ((Reduced|Raised) effort \((low|medium|high|xhigh|max) (→|->) (low|medium|high|xhigh|max)[;)]|Kept effort \((low|medium|high|xhigh|max)[;)]|(LOW|MEDIUM|HIGH) effort)'

# Cheap fast-exits for the graded path, BEFORE the git + jq repo-resolution work — so a
# per-turn Stop that has nothing new to report costs only a grep or two, not a full
# transcript parse. (The whole graded path requires the skill to have actually run.)
Expand All @@ -126,7 +129,7 @@ if [ "$label" = "graded" ]; then
# Has the set of verdict lines changed since we last processed a Stop? SKILL.md
# example lines are constant, so this count only rises on a real new verdict (incl.
# a phase-2 re-emit). Unchanged -> nothing to do -> skip the O(n) scrape + resolution.
vcount="$(grep -cE 'LinearB: .+ (LOW|MEDIUM|HIGH) effort' "$transcript" 2>/dev/null)"; vcount="${vcount:-0}"
vcount="$(grep -cE "$GRADED_RE" "$transcript" 2>/dev/null)"; vcount="${vcount:-0}"
vseen="$marker_dir/${session_id:-default}.vseen"
[ -f "$vseen" ] && [ "$(cat "$vseen" 2>/dev/null)" = "$vcount" ] && { dbg "Stop: skip: no new verdict since last report"; exit 0; }
fi
Expand Down Expand Up @@ -253,7 +256,7 @@ for line in open(sys.argv[1]):
# verdict must be ASSISTANT-authored text AFTER the Skill call — not the
# SKILL.md examples that load on invocation (they also match the pattern).
elif role == "assistant" and skill is not None and verdict is None \
and re.search(r"LinearB: .+ (LOW|MEDIUM|HIGH) effort", (x.get("text") or "") if typ == "text" else
and re.search(r"LinearB: .+ ((Reduced|Raised) effort \((low|medium|high|xhigh|max) (→|->) (low|medium|high|xhigh|max)[;)]|Kept effort \((low|medium|high|xhigh|max)[;)]|(LOW|MEDIUM|HIGH) effort)", (x.get("text") or "") if typ == "text" else
str((x.get("input") or {}).get("command", "")) if typ == "tool_use" and x.get("name") == "Bash"
and re.search(r"(^|\n|;|&&)\s*printf\s[^\n]*LinearB: ", str((x.get("input") or {}).get("command", ""))) else ""):
verdict = te
Expand Down Expand Up @@ -329,19 +332,34 @@ fi
# GATE 2 — a real assistant-authored verdict line exists, as text or as the hold's
# `printf` Bash call (grep, not python, so it still works without python3).
verdicts="$(jq -r 'select(.message.role? == "assistant") | .message.content? // [] | .[]? | if .type? == "text" then .text? // empty elif .type? == "tool_use" and .name? == "Bash" and ((.input.command? // "") | test("(^|\\n|;|&&)\\s*printf\\s[^\\n]*LinearB: ")) then .input.command else empty end' "$transcript" 2>/dev/null \
| grep -E 'LinearB: .+ (LOW|MEDIUM|HIGH) effort' || true)"
| grep -E "$GRADED_RE" || true)"
[ -n "$verdicts" ] || { dbg "$event: skip: skill ran but no verdict line yet"; exit 0; }

re='LinearB: (.+) (LOW|MEDIUM|HIGH) effort'
# Current shape: the change against the model's effort, e.g. "Raised effort (medium → high; …)" or "Kept effort (high; …)".
re_change='LinearB: (.+) (Reduced|Raised) effort \((low|medium|high|xhigh|max) (→|->) (low|medium|high|xhigh|max)[;)]'
re_kept='LinearB: (.+) Kept effort \((low|medium|high|xhigh|max)[;)]'
er='\(([^\)]*)\)'

# Parse a verdict line into repo/effort/evidence globals.
# Parse a verdict line into repo/effort/change/target/evidence globals; effort stays LOW/MEDIUM/HIGH.
parse_verdict() { # $1 = line
vrepo=""; veffort=""; vev=""
[[ "$1" =~ $re ]] || return 1
vrepo="${BASH_REMATCH[1]}"; veffort="${BASH_REMATCH[2]}"
vrepo=""; veffort=""; vchange=""; vtarget=""; vev=""
if [[ "$1" =~ $re_change ]]; then
vrepo="${BASH_REMATCH[1]}"; vtarget="${BASH_REMATCH[5]}"
if [ "${BASH_REMATCH[2]}" = Reduced ]; then vchange=reduced; else vchange=raised; fi
elif [[ "$1" =~ $re_kept ]]; then
vrepo="${BASH_REMATCH[1]}"; vtarget="${BASH_REMATCH[2]}"; vchange=kept
fi
if [ -n "$vchange" ]; then
case "$vtarget" in low) veffort=LOW ;; medium) veffort=MEDIUM ;; *) veffort=HIGH ;; esac
elif [[ "$1" =~ $re ]]; then
vrepo="${BASH_REMATCH[1]}"; veffort="${BASH_REMATCH[2]}"
else
return 1
fi
vrepo="${vrepo%% —}"; vrepo="${vrepo%%—}"; vrepo="${vrepo%"${vrepo##*[![:space:]]}"}"
[[ "$1" =~ $er ]] && vev="${BASH_REMATCH[1]}"
if [ -n "$vchange" ]; then case "$vev" in *\;*) vev="${vev#*; }" ;; *) vev="" ;; esac; fi
return 0
}
effort_val() { case "$1" in LOW) echo 1 ;; MEDIUM) echo 2 ;; HIGH) echo 3 ;; *) echo 0 ;; esac; }
Expand All @@ -363,6 +381,7 @@ report_decisions() {
--arg effort "$veffort" --arg repo "$vrepo" --arg ev "$vev" \
--arg email "$email" --arg repourl "$repo_url" --arg branch "$branch" \
--arg actual "$effort_actual" --arg ticket "$ticket" --arg sname "$session_name" \
--arg change "$vchange" --arg target "$vtarget" \
--arg gdur "$grading_duration" --arg model "$model" --arg gtok "$grading_tokens" --arg pver "$plugin_version" --arg idsrc "$identity_source" \
--arg backfill "$backfill" '
{
Expand All @@ -376,6 +395,7 @@ report_decisions() {
(if $ev != "" then {evidence: $ev} else {} end) +
(if $branch != "" then {branch: $branch} else {} end) +
(if $actual != "" then {actual_effort: $actual} else {} end) +
(if $change != "" then {effort_change: $change, target_effort: $target} else {} end) +
(if $ticket != "" then {ticket: $ticket} else {} end) +
(if $sname != "" then {session_name: $sname} else {} end) +
(if $gdur != "" then {grading_duration_s: $gdur} else {} end) +
Expand All @@ -397,7 +417,7 @@ report_decisions
# event == SessionEnd, graded: TOKENS event — the whole-session coding_tokens, attributed
# to the final (last) verdict. value 0 so it doesn't double-count on grade charts; the
# effort_level tag is kept for stratified coding_tokens comparison.
last_verdict="$(printf '%s\n' "$verdicts" | grep -E 'LinearB: .+ (LOW|MEDIUM|HIGH) effort' | tail -1)"
last_verdict="$(printf '%s\n' "$verdicts" | grep -E "$GRADED_RE" | tail -1)"
[ -n "$last_verdict" ] || exit 0
parse_verdict "$last_verdict" || exit 0
key="$(printf 'tokens|%s|%s' "$vrepo" "$veffort" | sha1)"
Expand Down
12 changes: 6 additions & 6 deletions plugins/agentic-advisor/hooks/agentic-advisor-trigger.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ skill_ran() {

# Is the LATEST advisor Skill call still missing its verdict (assistant text, or the
# hold's `printf` Bash call)? Prints "pending" or "ok"; prints nothing on parse failure.
VERDICT_RE='LinearB: .+ ((LOW|MEDIUM|HIGH) effort|skipped \(not a code task\))'
GRADED_RE='LinearB: .+ (LOW|MEDIUM|HIGH) effort'
VERDICT_RE='LinearB: .+ (((Reduced|Raised) effort \((low|medium|high|xhigh|max) (→|->) (low|medium|high|xhigh|max)[;)]|Kept effort \((low|medium|high|xhigh|max)[;)]|(LOW|MEDIUM|HIGH) effort)|skipped \(not a code task\))'
GRADED_RE='LinearB: .+ ((Reduced|Raised) effort \((low|medium|high|xhigh|max) (→|->) (low|medium|high|xhigh|max)[;)]|Kept effort \((low|medium|high|xhigh|max)[;)]|(LOW|MEDIUM|HIGH) effort)'
verdict_state() { # $1 = the verdict regex required (default: a grade or a skip)
jq -rs --arg n "agentic-advisor" --arg re "${1:-$VERDICT_RE}" '
[.[] | select(.message.role? == "assistant") | .message.content? // [] | .[]?] as $c
Expand Down Expand Up @@ -107,7 +107,7 @@ set_marker() { # $1 = a directory inside the repo
# Imperative, silent guidance — no permission question, no announcement.
skill_file="$(cd "$(dirname "$0")/.." 2>/dev/null && pwd)/skills/agentic-advisor/SKILL.md"
skill_hint=""; [ -f "$skill_file" ] && skill_hint=" If the skill is not in your skill list, read $skill_file and follow it."
guidance="MANDATORY FIRST STEP for this code task: before you read files, search, explore, spawn any subagent, or edit anything, you MUST invoke the \`agentic-advisor\` skill (via the Skill tool) to grade this repo's effort level. Do not start the task, do not touch code, until you have run it and printed its verdict line verbatim in this shape: > LinearB: <repo> — <LOW|MEDIUM|HIGH> effort (<evidence>) — <plan> (a hook parses that line; a prose summary is not recorded). This is required, not optional — do not skip it even if the task seems obvious. Invoke it directly (no need to ask the user or announce it). It degrades gracefully: if LinearB is unavailable, default to MEDIUM effort and continue. If the task will not change any code, print > LinearB: <repo> — skipped (not a code task) instead of a grade.$skill_hint"
guidance="MANDATORY FIRST STEP for this code task: before you read files, search, explore, spawn any subagent, or edit anything, you MUST invoke the \`agentic-advisor\` skill (via the Skill tool) to grade this repo's effort level. Do not start the task, do not touch code, until you have run it and printed its verdict line verbatim in this shape: > LinearB: <repo> — <Raised|Reduced> effort (<current> → <new level>; <evidence>) — <plan>, or > LinearB: <repo> — Kept effort (<current>; <evidence>) — <plan> (a hook parses that line; a prose summary is not recorded). This is required, not optional — do not skip it even if the task seems obvious. Invoke it directly (no need to ask the user or announce it). It degrades gracefully: if LinearB is unavailable, work at medium effort and continue. If the task will not change any code, print > LinearB: <repo> — skipped (not a code task) instead of a grade.$skill_hint"

# Mark handled (this session+repo) and emit additionalContext for the event.
emit() { # $1 = hookEventName, $2 = context text
Expand Down Expand Up @@ -173,7 +173,7 @@ case "$event" in
state="$(verdict_state)"
if [ -n "$state" ] && : > "$vdone" 2>/dev/null && [ "$state" = "pending" ]; then
dbg "PreToolUse: reminded the agent to print the verdict line"
note "LinearB: your effort grade isn't recorded yet. It's only recorded from the verdict line, not from thinking, so print it now (a prose summary doesn't count): > LinearB: <repo> — <LOW|MEDIUM|HIGH> effort (<evidence>) — <plan>. If you aren't writing text, run: printf '%s\\n' '> LinearB: <repo> — <LEVEL> effort (<evidence>) — <plan>'. If no code will change, print > LinearB: <repo> — skipped (not a code task) instead."
note "LinearB: your effort grade isn't recorded yet. It's only recorded from the verdict line, not from thinking, so print it now (a prose summary doesn't count): > LinearB: <repo> — <Raised|Reduced> effort (<current> → <new level>; <evidence>) — <plan>, or > LinearB: <repo> — Kept effort (<current>; <evidence>) — <plan>. If you aren't writing text, run: printf '%s\\n' '> LinearB: <repo> — <that verdict>'. If no code will change, print > LinearB: <repo> — skipped (not a code task) instead."
fi
fi
fi
Expand Down Expand Up @@ -234,9 +234,9 @@ case "$event" in
printf '%s' "$((n + 1))" > "$held" 2>/dev/null || exit 0
dbg "PreToolUse: held $tool until a verdict line exists (hold $((n + 1)))"
if skill_ran; then
reason="Your effort grade isn't recorded yet: print the verdict line itself, not a prose summary — > LinearB: <repo> — <LOW|MEDIUM|HIGH> effort (<evidence>) — <plan> (or run: printf '%s\\n' '> LinearB: …'). A skipped (not a code task) line doesn't count once you change code. Then make the same call again."
reason="Your effort grade isn't recorded yet: print the verdict line itself, not a prose summary — > LinearB: <repo> — <Raised|Reduced> effort (<current> → <new level>; <evidence>) — <plan>, or > LinearB: <repo> — Kept effort (<current>; <evidence>) — <plan> (or run: printf '%s\\n' '> LinearB: …'). A skipped (not a code task) line doesn't count once you change code. Then make the same call again."
else
reason="One quick first step before ${what}: run the \`agentic-advisor\` skill to grade this repo's effort.$skill_hint Print its verdict line (> LinearB: <repo> — <LOW|MEDIUM|HIGH> effort (…) — …), then make the same call again; this hold repeats until it's printed. If LinearB isn't available it just defaults to MEDIUM."
reason="One quick first step before ${what}: run the \`agentic-advisor\` skill to grade this repo's effort.$skill_hint Print its verdict line (> LinearB: <repo> — <Raised|Reduced> effort (<current> → <new level>; <evidence>) — <plan>, or > LinearB: <repo> — Kept effort (<current>; <evidence>) — <plan>), then make the same call again; this hold repeats until it's printed. If LinearB isn't available it just works at medium effort."
fi
[ "$what" = "this edit" ] || reason="$reason Tell the subagent the effort level in its prompt, so the delegated work follows it."
deny "$reason"
Expand Down
Loading
Loading