diff --git a/plugins/agentic-advisor/.claude-plugin/plugin.json b/plugins/agentic-advisor/.claude-plugin/plugin.json index a0d9b6e..b3e0d77 100644 --- a/plugins/agentic-advisor/.claude-plugin/plugin.json +++ b/plugins/agentic-advisor/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentic-advisor", - "version": "1.4.5", + "version": "1.4.6", "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.", "author": { "name": "LinearB", diff --git a/plugins/agentic-advisor/CHANGELOG.md b/plugins/agentic-advisor/CHANGELOG.md index 7a7272b..687bcec 100644 --- a/plugins/agentic-advisor/CHANGELOG.md +++ b/plugins/agentic-advisor/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.4.6] - 2026-10-21 + +### Changed + +- A task that changes no code (reading, answering a question, reviewing a doc or screenshot, writing a comment) is no longer graded LOW: the skill prints `> LinearB: — skipped (not a code task)`, makes no calls, reports nothing, and the agent works with its normal care. LOW stays for trivial code edits (comments, formatting, renames). +- The skill states that the effort level governs code changes only and never lowers the care of reading, analysis or writing. +- "Skipped" means the task modifies no file; any file edit with no behavior change (code comments, doc files, formatting) is still LOW. The edit hold now waits for a real grade, so code work that follows a skipped run is graded first. + ## [1.4.5] - 2026-10-18 ### Changed diff --git a/plugins/agentic-advisor/README.md b/plugins/agentic-advisor/README.md index 344c5ae..9ae32ac 100644 --- a/plugins/agentic-advisor/README.md +++ b/plugins/agentic-advisor/README.md @@ -30,7 +30,7 @@ The agent prints a one-line verdict before writing code, e.g.: - **Effort bands** match LinearB's own rework benchmark: ELITE <3% / STRONG 3–6% → **LOW**; FAIR 6–7% → **MEDIUM**; NEEDS FOCUS >7% → **HIGH**. Effort = the highest-firing signal (rework, unreviewed merges, or a recent serious incident). - **Task complexity:** the request itself is graded too (scope, breadth, tricky logic, blast radius), so a demanding change in a calm repo still gets real care. If the prompt or branch names a ticket, its type, priority and estimate from LinearB count too (a Highest-priority bug raises the grade). The final effort is the highest of repo health, task complexity and (when it runs) phase 2. - **Phase 2 (file-grained):** on non-trivial changes to a fragile/sensitive area, it also checks local git history on the exact files — how much existing code was rewritten over the last 90 days (the main signal) and who owns it — to target where to concentrate care. Commit messages saying "fix"/"revert" count only as a weak hint. -- **Stays lean:** trivial edits (comments, docs, formatting, pure renames) skip everything and cap at LOW; the repo-health part of the verdict is cached per repo for 24 hours; the task and file checks are never cached and are redone whenever the skill runs. +- **Stays lean:** a task that modifies no file (questions, reviews, drafting a reply) is not graded at all; edits with no behavior change (code comments, doc files, formatting, pure renames) skip everything and cap at LOW; the repo-health part of the verdict is cached per repo for 24 hours; the task and file checks are never cached and are redone whenever the skill runs. ## Triggers diff --git a/plugins/agentic-advisor/hooks/agentic-advisor-trigger.sh b/plugins/agentic-advisor/hooks/agentic-advisor-trigger.sh index 2b2197a..ee830e0 100755 --- a/plugins/agentic-advisor/hooks/agentic-advisor-trigger.sh +++ b/plugins/agentic-advisor/hooks/agentic-advisor-trigger.sh @@ -52,8 +52,10 @@ 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_state() { - jq -rs --arg n "agentic-advisor" --arg re 'LinearB: .+ (LOW|MEDIUM|HIGH) effort' ' +VERDICT_RE='LinearB: .+ ((LOW|MEDIUM|HIGH) effort|skipped \(not a code task\))' +GRADED_RE='LinearB: .+ (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 | [$c | to_entries[] | select(.value.type? == "tool_use" and .value.name? == "Skill" and ((.value.input.skill? // "") | tostring | contains($n))) | .key] as $sk @@ -94,7 +96,7 @@ set_marker() { # $1 = a directory inside the repo } # Imperative, silent guidance — no permission question, no announcement. -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: — effort () — (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." +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: — effort () — (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: — skipped (not a code task) instead of a grade." # Mark handled (this session+repo) and emit additionalContext for the event. emit() { # $1 = hookEventName, $2 = context text @@ -158,7 +160,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: > LinearB: — effort () — . If you aren't writing text, run: printf '%s\\n' '> LinearB: — effort () — '" + note "LinearB: your effort grade isn't recorded yet. It's only recorded from the verdict line, not from thinking, so print it now: > LinearB: — effort () — . If you aren't writing text, run: printf '%s\\n' '> LinearB: — effort () — '. If no code will change, print > LinearB: — skipped (not a code task) instead." fi fi fi @@ -196,8 +198,10 @@ case "$event" in # (missing file path, non-git, missing transcript, or marker write failure). git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0 - # Skill already ran this session -> allow the edit. If a verdict names this repo, mark it handled so later prompts here aren't nudged. - if skill_ran; then + # Skill already graded this session -> allow the edit. A "skipped (not a code task)" line alone doesn't count: editing now is code work. + # If a verdict names this repo, mark it handled so later prompts here aren't nudged. + # "pending" only when parsed and no grade yet; a parse failure prints nothing and fails open. + if skill_ran && [ "$(verdict_state "$GRADED_RE")" != "pending" ]; then set_marker "$dir" # The verdict uses the repo's name: check the remote's name and the folder's (clones and worktrees can differ). url="$(git -C "$dir" config --get remote.origin.url 2>/dev/null)"; url="${url%.git}" diff --git a/plugins/agentic-advisor/skills/agentic-advisor/SKILL.md b/plugins/agentic-advisor/skills/agentic-advisor/SKILL.md index 0fee01d..69c4807 100644 --- a/plugins/agentic-advisor/skills/agentic-advisor/SKILL.md +++ b/plugins/agentic-advisor/skills/agentic-advisor/SKILL.md @@ -22,6 +22,7 @@ Right after the repo sweep, your first assistant text **must be this verdict lin - **A hook parses this exact line** to record the decision. Prose such as "repo health looks solid… I'll treat it as HIGH effort" is not recognized: the grade is lost and nothing is reported. - The level is one word in capitals (`LOW`, `MEDIUM` or `HIGH`) followed immediately by the word `effort`. - Explanation may follow on later lines, but the verdict line comes first and is never reworded. +- **No code will change?** Print `> LinearB: — skipped (not a code task)` instead of a grade and stop (see the cheap exits). Nothing is reported. - If you aren't writing visible text at this point, record it with one Bash call instead: `printf '%s\n' '> LinearB: — effort () — '`. If neither exists, a hook reminds you on your next file read or edit. Example: `> LinearB: api-service — HIGH effort (rework 3.4%, calm repo — but auth refactor with wide blast radius) — small behavior-preserving steps, tests green, confirm scope first.` @@ -44,7 +45,8 @@ set -o pipefail # with curl -f, a failed request fails the whole pipeline inst Two checks that prevent wasted work — do them before any LinearB call: -- **Trivial edit → skip everything.** If the task changes **no code behavior** (comments, docs, formatting/whitespace, a pure rename), skip signal gathering entirely: effort is **LOW**, add no defensive guards (there's no logic to protect), and don't call LinearB at all — this holds **even on a fragile repo**. Anything that changes behavior is **not** trivial and warrants the sweep below — note that a **constant-value change counts as behavioral** (e.g. a timeout, retry count, or feature-flag default), as does any conditional or logic edit. +- **Not a code task → no grade at all.** If the task will not modify any file (reading or explaining code, answering a question, reviewing a doc, ticket or screenshot, drafting a reply or message in chat), don't grade it: print `> LinearB: — skipped (not a code task)`, call nothing, and work with your normal care. A grade measures the risk of a code change; a LOW here would wrongly tell you to go light on accuracy. If the task later turns into a file edit, run the sweep then, before the first edit (a hook holds that edit until a real grade exists). +- **Trivial code edit → LOW, skip everything.** If the task edits any file but changes **no code behavior** (code comments, doc files, formatting/whitespace, a pure rename), skip signal gathering entirely: effort is **LOW**, add no defensive guards (there's no logic to protect), and don't call LinearB at all — this holds **even on a fragile repo**. Anything that changes behavior is **not** trivial and warrants the sweep below — note that a **constant-value change counts as behavioral** (e.g. a timeout, retry count, or feature-flag default), as does any conditional or logic edit. - **Already graded recently → reuse (persistent 24h cache).** The repo-level verdict is daily-stable, so compute it at most **once per repo per ~24h**, not once per task/session — this is the main token/time saver. Cache file, keyed by repo root: ```text @@ -260,6 +262,8 @@ Phase 2 was only ever deferred because it needs the target file paths. So: ## Coding Posture +The effort level governs **code changes only**: it never lowers the care, accuracy or thoroughness of reading, analysis, answers or writing. + Use the effort level to guide the implementation silently. The aim is to **spend effort proportional to repo health** — go light (saving tokens and time) on calm repos, dig in on fragile ones. Don't waste effort. LOW effort — repo confirmed healthy, go light and token-efficient (an active repo can still be LOW — busy ≠ risky): diff --git a/tests/agentic-advisor/hooks_test.sh b/tests/agentic-advisor/hooks_test.sh index ccf61de..9bcbc7c 100755 --- a/tests/agentic-advisor/hooks_test.sh +++ b/tests/agentic-advisor/hooks_test.sh @@ -53,6 +53,9 @@ reminded() { [ "$rc" -eq 0 ] && jq -e '(.hookSpecificOutput.permissionDecision = denied() { [ "$rc" -eq 0 ] && jq -e '.hookSpecificOutput.permissionDecision == "deny"' <<<"$out" >/dev/null; } add_skill() { printf '%s\n' '{"timestamp":"2026-01-01T00:00:00Z","message":{"role":"assistant","model":"m","usage":{"output_tokens":10},"content":[{"type":"tool_use","name":"Skill","input":{"skill":"agentic-advisor:agentic-advisor"}}]}}' >> "$TRANSCRIPT"; } add_verdict() { printf '%s\n' '{"timestamp":"2026-01-01T00:00:05Z","message":{"role":"assistant","model":"m","usage":{"output_tokens":20},"content":[{"type":"text","text":"> LinearB: widgets — MEDIUM effort (rework 6.5%) — focused diff."}]}}' >> "$TRANSCRIPT"; } +# The loaded SKILL.md lands in the transcript as a user message, example verdict line included. +add_skill_body() { printf '%s\n' '{"type":"user","isMeta":true,"message":{"role":"user","content":[{"type":"text","text":"Example: > LinearB: api-service — HIGH effort (rework 3.4%) — small steps."}]}}' >> "$TRANSCRIPT"; } +add_skipped() { printf '%s\n' '{"message":{"role":"assistant","content":[{"type":"text","text":"> LinearB: widgets — skipped (not a code task)"}]}}' >> "$TRANSCRIPT"; } add_verdict_for() { printf '{"message":{"role":"assistant","content":[{"type":"text","text":"> LinearB: %s — MEDIUM effort (x) — y."}]}}\n' "$1" >> "$TRANSCRIPT"; } # Reporter posts from a detached subshell; wait briefly for the stub to record calls. ncalls() { find "$T/curl" -type f | wc -l | tr -d ' '; } @@ -171,6 +174,21 @@ run "$TRIGGER" PreToolUse "$(tool Read app.js)" check "trigger: skill ran without verdict -> reminder, not a block" reminded teardown +setup +add_skill; add_skipped +run "$TRIGGER" PreToolUse "$(tool Read app.js)" +check "trigger: a skipped (not a code task) line counts as answered -> no reminder" silent +teardown + +setup +add_skill; add_skill_body; add_skipped +run "$TRIGGER" PreToolUse "$(tool Edit app.js)" +check "trigger: an edit after a skipped (not a code task) run is held until a real grade" denied +add_verdict +run "$TRIGGER" PreToolUse "$(tool Edit app.js)" +check "trigger: ...and goes through once the grade is printed" silent +teardown + setup add_skill; add_verdict run "$TRIGGER" PreToolUse "$(tool Edit app.js)" @@ -195,6 +213,13 @@ await_calls 2 # decision + tokens, posted concurrently check "report: SessionEnd backfills a missed DECISION" backfilled teardown +setup +add_skill; add_skipped +run "$REPORT" SessionEnd +sleep 0.5 # a wrong post would arrive from a detached subshell +check "report: a skipped (not a code task) session reports nothing" no_calls +teardown + # Both exit before any background post, so no wait is needed. setup add_skill; add_verdict