Skip to content

feat: add qyl-continuation plugin (smart auto-continuation, v1.0.0) (#158)#158

Merged
ANcpLua merged 1 commit intomainfrom
feat/qyl-continuation
Mar 6, 2026
Merged

feat: add qyl-continuation plugin (smart auto-continuation, v1.0.0) (#158)#158
ANcpLua merged 1 commit intomainfrom
feat/qyl-continuation

Conversation

@ANcpLua
Copy link
Owner

@ANcpLua ANcpLua commented Mar 6, 2026

qyl-continuation plugin (1.0.0)

Smart auto-continuation for Claude Code. Two-phase stop hook:

Phase 1 — Heuristics (no LLM call, ~80% of stops):

  • H1: Assistant asked user a question → stop
  • H2: Completion signal without stated next steps → stop
  • H3: Tool results already addressed → stop
  • H4: Substantial text-only response → stop

Phase 2 — Haiku judge (~20% of cases):

  • Structured JSON: should_continue + reasoning
  • Throttled: max 3 continuations per 5-minute window

Based on double-shot-latte (MIT).

Changes

  • New plugin: plugins/qyl-continuation/
  • Registered in marketplace.json (12 → 13 plugins)
  • CHANGELOG updated

Summary by CodeRabbit

  • New Features
    • Added qyl-continuation plugin with smart auto-continuation capability
    • Two-phase continuation system: heuristic pre-filter reduces unnecessary calls by ~80%; Haiku judge handles edge cases
    • Throttling limit: max 3 continuations per 5-minute window
    • Updated marketplace to reflect new plugin (total: 13 plugins)

Copilot AI review requested due to automatic review settings March 6, 2026 19:06
@ANcpLua ANcpLua merged commit 57f8db0 into main Mar 6, 2026
6 of 8 checks passed
@ANcpLua ANcpLua deleted the feat/qyl-continuation branch March 6, 2026 19:06
@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 02768162-d2a4-47c3-a8e4-59b6dd948f48

📥 Commits

Reviewing files that changed from the base of the PR and between 12e49e7 and 42cf6e2.

📒 Files selected for processing (6)
  • .claude-plugin/marketplace.json
  • CHANGELOG.md
  • plugins/qyl-continuation/.claude-plugin/plugin.json
  • plugins/qyl-continuation/README.md
  • plugins/qyl-continuation/hooks/hooks.json
  • plugins/qyl-continuation/hooks/stop-judge.py

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting


📝 Walkthrough

Walkthrough

The PR introduces a new "qyl-continuation" plugin that implements smart auto-continuation for Claude Code using a two-phase mechanism: heuristic-based pre-filtering to eliminate unnecessary LLM calls, followed by a Haiku judge for inconclusive cases. The plugin is registered in the marketplace, includes configuration and documentation, and implements throttling logic to limit continuations.

Changes

Cohort / File(s) Summary
Marketplace & Changelog
.claude-plugin/marketplace.json, CHANGELOG.md
Updated marketplace metadata to reflect new qyl-continuation plugin (version 1.0.0) and incremented plugin count from 12 to 13; added plugin description highlighting two-phase stop hook and throttling limits.
Plugin Metadata
plugins/qyl-continuation/.claude-plugin/plugin.json, plugins/qyl-continuation/README.md
Added plugin manifest with name, version, author, and license metadata; included comprehensive README documenting two-phase operation (Phase 1 heuristics, Phase 2 Haiku judge), environment variables, and output constraints (max 3 continuations per 5-minute window).
Plugin Implementation
plugins/qyl-continuation/hooks/hooks.json, plugins/qyl-continuation/hooks/stop-judge.py
Configured Stop hook to execute Python-based stop-judge with 35-second timeout; implemented decision logic with heuristic filters (last message is question, signals completion, addresses tool result, substantial text-only), Haiku-based adjudication, per-session throttling (max 3 continuations in 300-second window), and error-resilient fallback behavior.

Sequence Diagram

sequenceDiagram
    participant User as Claude Session
    participant Hook as Stop Hook
    participant Judge as stop-judge.py
    participant Heuristic as Phase 1: Heuristics
    participant Haiku as Phase 2: Haiku Judge
    participant Throttle as Throttle State
    
    User->>Hook: Trigger Stop Hook
    Hook->>Judge: Execute with transcript
    Judge->>Judge: Read transcript tail (last 6 lines)
    Judge->>Heuristic: Analyze message patterns
    
    alt Heuristic Match
        Heuristic->>Judge: Decision reached
        Judge->>Judge: Approve stop
    else Heuristic Inconclusive
        Judge->>Haiku: Send context to Haiku judge
        alt Judge Success
            Haiku->>Judge: Return continue/stop decision
            Judge->>Throttle: Check throttle state
            alt Should Continue & Within Limit
                Throttle->>Judge: Increment counter
                Judge->>Judge: Block with reason
            else Limit Exceeded or Should Stop
                Throttle->>Judge: Clear counter
                Judge->>Judge: Approve stop
            end
        else Judge Timeout/Error
            Judge->>Judge: Fallback to approve stop
        end
    end
    
    Judge->>Hook: Return decision (approve/block)
    Hook->>User: Control flow decision
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/qyl-continuation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request integrates a new qyl-continuation plugin into the Claude Code ecosystem. The plugin aims to enhance the auto-continuation feature by intelligently deciding when to stop or continue, significantly reducing unnecessary LLM calls through a heuristic-based pre-filter and a throttled Haiku judge for more complex scenarios. This improves efficiency and user experience by making Claude's continuation behavior more context-aware.

Highlights

  • New Plugin: qyl-continuation: Introduced the qyl-continuation plugin, providing smart auto-continuation capabilities for Claude Code.
  • Two-Phase Stop Hook: Implemented a two-phase stop hook system: a heuristic pre-filter handles approximately 80% of stop decisions without an LLM call, and a 'Haiku judge' LLM handles the remaining ambiguous cases.
  • Marketplace and Changelog Updates: Registered the new plugin in the marketplace and added a detailed entry to the CHANGELOG.md.
Changelog
  • .claude-plugin/marketplace.json
    • Registered the new qyl-continuation plugin.
    • Updated the marketplace description to reflect the increased plugin count and the addition of smart auto-continuation.
  • CHANGELOG.md
    • Added a detailed entry for the qyl-continuation plugin, describing its two-phase stop hook and throttling mechanism.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new plugin, qyl-continuation, for smart auto-continuation of Claude conversations, implemented with a two-phase approach involving heuristic checks and an LLM-based judge. A security audit of the stop-judge.py script revealed critical vulnerabilities, specifically an insecure temporary file vulnerability in /tmp that could enable symlink attacks and arbitrary file overwrites, and a path traversal vulnerability due to insufficient validation of the transcript_path input. Furthermore, the plugin's overall robustness can be enhanced by improving data handling and parsing.

event = json.loads(sys.stdin.read())
transcript_path = event.get("transcript_path", "")
session_id = event.get("session_id", "unknown")
throttle_file = Path(f"/tmp/.qyl-continue-{session_id.replace('/', '_')}")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The script creates a throttle file in /tmp with a predictable name based on the session_id. In multi-user environments, an attacker can pre-create this file as a symlink to an arbitrary file owned by the user, leading to an arbitrary file overwrite when the script writes to the throttle file. Additionally, an attacker can manipulate the throttle state for other users. It is recommended to use a user-specific directory for such files, such as ~/.claude/qyl-continuation/.

Suggested change
throttle_file = Path(f"/tmp/.qyl-continue-{session_id.replace('/', '_')}")
work_dir = Path.home() / ".claude" / "qyl-continuation"
work_dir.mkdir(parents=True, exist_ok=True)
throttle_file = work_dir / f"throttle-{session_id.replace('/', '_')}"

Comment on lines +74 to +75
if not transcript_path or not Path(transcript_path).exists():
approve("No transcript")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The transcript_path provided in the event input is used directly to read a file without validation. An attacker who can influence the event data could potentially point this to arbitrary files on the system. While the script only reads the last few lines and expects JSONL format, it still constitutes a path traversal vulnerability. Other plugins in this repository (like hookify) explicitly validate this path to prevent access outside of intended directories.

Suggested change
if not transcript_path or not Path(transcript_path).exists():
approve("No transcript")
if not transcript_path or not Path(transcript_path).exists() or os.path.isabs(transcript_path) or '..' in transcript_path.split(os.path.sep):
approve("Invalid or missing transcript")


# --- Phase 2: Haiku judge ---

transcript_json = json.dumps(messages, ensure_ascii=False)[:MAX_CONTEXT_BYTES]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Slicing a JSON string by bytes with [:MAX_CONTEXT_BYTES] is unsafe as it can create invalid JSON by cutting in the middle of a multi-byte character or a structural element. This could cause the downstream claude process to fail when parsing its input. A safer approach is to encode to bytes, slice, and then decode while ignoring errors. This is still not perfectly safe for the JSON structure but does handle character encoding issues correctly.

Suggested change
transcript_json = json.dumps(messages, ensure_ascii=False)[:MAX_CONTEXT_BYTES]
transcript_json = json.dumps(messages, ensure_ascii=False).encode('utf-8')[:MAX_CONTEXT_BYTES].decode('utf-8', 'ignore')

Comment on lines +223 to +225
evaluation = json.loads(proc.stdout).get("structured_output", {})
except (json.JSONDecodeError, AttributeError, TypeError):
approve("Judge response unparseable, allowing stop")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The parsing of the judge's output can be made more robust. If structured_output is present but null, get("structured_output", {}) returns None, causing an AttributeError on the next line. While the broad except block catches this, handling this case explicitly would make the code clearer and less reliant on exception handling for control flow.

Suggested change
evaluation = json.loads(proc.stdout).get("structured_output", {})
except (json.JSONDecodeError, AttributeError, TypeError):
approve("Judge response unparseable, allowing stop")
output = json.loads(proc.stdout)
evaluation = (output.get("structured_output") or {}) if isinstance(output, dict) else {}
except json.JSONDecodeError:
approve("Judge response unparseable, allowing stop")

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new qyl-continuation plugin to the marketplace to provide “smart auto-continuation” behavior for Claude Code via a two-phase Stop hook (heuristics first, then an LLM judge with throttling).

Changes:

  • Introduces plugins/qyl-continuation/ with a Stop hook implementation (stop-judge.py) and hook registration (hooks.json).
  • Registers the new plugin in .claude-plugin/marketplace.json.
  • Documents the addition in CHANGELOG.md and adds plugin-level README/config docs.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
plugins/qyl-continuation/hooks/stop-judge.py Implements heuristic + judge-based Stop hook with throttling and transcript parsing.
plugins/qyl-continuation/hooks/hooks.json Registers the Stop hook command to run the judge script.
plugins/qyl-continuation/README.md Documents behavior and environment configuration.
plugins/qyl-continuation/.claude-plugin/plugin.json Adds plugin manifest metadata for distribution.
CHANGELOG.md Adds an “Unreleased” entry describing the new plugin.
.claude-plugin/marketplace.json Updates marketplace metadata/counts and registers the new plugin source.



def has_block_type(msg: dict, block_type: str) -> bool:
content = msg.get("content", "")
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has_block_type() checks msg.get("content"), but transcript entries for tool_use/tool_result blocks are typically nested under message.content. This makes the tool-result/pending-tool heuristics (H3/H4) effectively inoperative. Consider checking both msg.get("message", {}).get("content") and msg.get("content") for compatibility.

Suggested change
content = msg.get("content", "")
content = msg.get("content", "")
if not isinstance(content, list):
# Some transcript entries nest blocks under message.content
content = msg.get("message", {}).get("content", "")

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +69
if event.get("stop_hook_active", False):
count, last_time = read_throttle()
if time.time() - last_time > WINDOW_SECONDS:
count = 0
if count >= MAX_CONTINUATIONS:
clear_throttle()
approve("Max continuation cycles reached")
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The throttle guard is gated on event.get("stop_hook_active"), but this field doesn’t appear to be part of the Stop hook payload elsewhere in the repo (no other hooks reference it). If it’s absent at runtime, this early-exit will never run, and continuation cycles may never be capped. Recommend enforcing the throttle based on the persisted counter alone (or gate on a verified input field) so the max-cycles protection is reliable.

Suggested change
if event.get("stop_hook_active", False):
count, last_time = read_throttle()
if time.time() - last_time > WINDOW_SECONDS:
count = 0
if count >= MAX_CONTINUATIONS:
clear_throttle()
approve("Max continuation cycles reached")
count, last_time = read_throttle()
if time.time() - last_time > WINDOW_SECONDS:
count = 0
if count >= MAX_CONTINUATIONS:
clear_throttle()
approve("Max continuation cycles reached")

Copilot uses AI. Check for mistakes.

# --- Phase 2: Haiku judge ---

transcript_json = json.dumps(messages, ensure_ascii=False)[:MAX_CONTEXT_BYTES]
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_CONTEXT_BYTES is enforced via json.dumps(... )[:MAX_CONTEXT_BYTES], but slicing a Python str limits characters, not UTF-8 bytes. With non-ASCII content this can exceed the intended byte budget (or cut mid-codepoint). If you want a true byte limit, encode to UTF-8, slice bytes, then decode with errors="ignore"/"replace" (or rename the constant to reflect char-count semantics).

Suggested change
transcript_json = json.dumps(messages, ensure_ascii=False)[:MAX_CONTEXT_BYTES]
raw_transcript = json.dumps(messages, ensure_ascii=False)
transcript_bytes = raw_transcript.encode("utf-8")
if len(transcript_bytes) > MAX_CONTEXT_BYTES:
transcript_json = transcript_bytes[:MAX_CONTEXT_BYTES].decode("utf-8", errors="ignore")
else:
transcript_json = raw_transcript

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +25
| Environment Variable | Default | Description |
|---------------------|---------|-------------|
| `QYL_CONTINUATION_TIMEOUT` | `30` | Haiku judge timeout (seconds) |
| `QYL_CONTINUATION_MODEL` | `haiku` | Judge model |
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The configuration table uses || to start rows, which breaks standard Markdown table syntax (it should be single | ... | ... | ... |). As-is, this won’t render as a table in most Markdown viewers/linters.

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +113
def text_of(msg: dict) -> str:
content = msg.get("content", "")
if isinstance(content, str):
return content
if isinstance(content, list):
return " ".join(
b.get("text", "") or b.get("content", "")
for b in content
if isinstance(b, dict) and b.get("type") in ("text", "tool_result")
)
return ""
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text_of() reads msg.get("content"), but the Claude transcript JSONL entries appear to store the payload under message.content (see e.g. metacognitive-guard Stop hook parsing ... | .message.content[]). As written, last_assistant will usually be empty, so H1/H2/H4 won’t trigger and the judge prompt context will be less accurate. Update text_of() to extract text/tool_result blocks from msg.get("message", {}).get("content", ...) (and keep the current fallback for any legacy formats).

Copilot uses AI. Check for mistakes.
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42cf6e2b9c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +167 to +169
if addressed and last_role == "assistant":
clear_throttle()
approve("Heuristic: tool results already addressed")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow continuation when assistant states pending next action

This heuristic unconditionally approves stopping whenever any tool result has been followed by a long assistant message and the last message is from the assistant, but it never checks for explicit pending-next-step language (e.g., “Next I’ll run tests”). In that common flow, the hook exits here before the Haiku judge runs, so the session stops even though the assistant explicitly indicated autonomous work remains.

Useful? React with 👍 / 👎.

Comment on lines +78 to +80
tail = subprocess.run(
["tail", "-n", str(TAIL_LINES), transcript_path],
capture_output=True, text=True, timeout=5

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove shell tail dependency from transcript loading

The hook shells out to tail to read transcript lines; on environments without that binary (notably non-Unix setups), this path falls into the exception handler and approves stopping with "Failed to read transcript", effectively disabling continuation for the entire plugin. Reading the file tail in Python would avoid this platform-specific failure mode.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants