Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
45456a0
feat(scripts): add interactive batch task creator from discovery outputs
qvidal01 Feb 14, 2026
0a1ae80
feat: add ac-phase script, fix worktree branch sanitization, install …
qvidal01 Feb 15, 2026
ca88820
feat: add preflight self-healing hook to CLI and runner entry points
qvidal01 Feb 15, 2026
304995d
fix: handle milliseconds in ISO 8601 rate limit reset timestamps
qvidal01 Feb 15, 2026
905d8e2
feat(scripts): add ac-batch interactive batch task manager
qvidal01 Feb 16, 2026
fedaf42
fix: update ac-phase to reference ac-batch instead of batch-from-disc…
qvidal01 Feb 16, 2026
06f90f1
feat: add insights chat to ac-batch workflow
qvidal01 Feb 16, 2026
6a9dc4b
feat: add ideation and roadmap to ac-batch workflow
qvidal01 Feb 16, 2026
426cde6
fix: sanitize slashes and special chars from batch spec directory names
qvidal01 Feb 16, 2026
d88744c
fix: preserve category tags in batch spec directory names for ac-phase
qvidal01 Feb 16, 2026
b718374
docs: add ac-batch and ac-phase quick reference guide
qvidal01 Feb 16, 2026
9c52252
fix: pass --force to bypass approval check in ac-batch builds
qvidal01 Feb 16, 2026
a86a7e2
fix: pass --force to bypass approval check in ac-phase builds
qvidal01 Feb 16, 2026
1e3e2c3
fix: handle unknown SDK message types (e.g. rate_limit_event) gracefully
qvidal01 Feb 19, 2026
3276395
fix: track stuck subtasks in-memory to avoid re-processing failures
qvidal01 Feb 20, 2026
4d8cae4
fix: improve ac-batch project detection and non-interactive support
qvidal01 Feb 20, 2026
22bc0d5
chore: update npm dependencies and ignore Windows path artifacts
qvidal01 Feb 20, 2026
5513395
fix: address PR #1846 review feedback for portability and security
qvidal01 Feb 20, 2026
83b9929
merge: resolve conflicts with upstream develop
qvidal01 Feb 20, 2026
cd37697
fix: resolve Ruff lint errors (f-string placeholder, import sorting)
qvidal01 Feb 20, 2026
4c20549
fix: resolve CI failures (Ruff format, SDK import in tests)
qvidal01 Feb 20, 2026
2705682
style: apply ruff format to all flagged backend files
qvidal01 Feb 20, 2026
a143f52
fix: patch run_preflight in CLI tests to prevent SystemExit in CI
qvidal01 Feb 20, 2026
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ playwright/.cache/
*.tmp
*.temp

# Windows path artifacts (created when Windows symlinks leak into WSL/git)
C:\\

# Development
dev/
_bmad/
Expand All @@ -175,3 +178,5 @@ OPUS_ANALYSIS_AND_IDEAS.md
/shared_docs
logs/security/
Agents.md
auto-claude-desktop.sh
/images/
13 changes: 13 additions & 0 deletions apps/backend/agents/coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,7 @@ def _validate_and_fix_implementation_plan() -> tuple[bool, list[str]]:
concurrency_error_context: str | None = (
None # Context to pass to agent after concurrency error
)
stuck_subtask_ids_local: set[str] = set() # In-memory tracking of stuck subtasks

def _reset_concurrency_state() -> None:
"""Reset concurrency error tracking state after a successful session or non-concurrency error."""
Expand Down Expand Up @@ -937,6 +938,13 @@ def _reset_concurrency_state() -> None:
# Get the next subtask to work on (planner sessions shouldn't bind to a subtask)
next_subtask = None if first_run else get_next_subtask(spec_dir)
subtask_id = next_subtask.get("id") if next_subtask else None

# Skip subtasks already marked as stuck in this session
if subtask_id and subtask_id in stuck_subtask_ids_local:
# Try to find a non-stuck subtask by marking this one in the plan
next_subtask = None
subtask_id = None
Comment on lines +942 to +946

This comment was marked as outdated.


phase_name = next_subtask.get("phase_name") if next_subtask else None

# Update status for this session
Expand Down Expand Up @@ -1109,6 +1117,7 @@ def _reset_concurrency_state() -> None:
subtask_id,
f"File validation failed after {attempt_count} attempts: {error_msg}",
)
stuck_subtask_ids_local.add(subtask_id)
emit_phase(
ExecutionPhase.FAILED,
f"Subtask {subtask_id} stuck: file validation failed",
Expand All @@ -1123,6 +1132,8 @@ def _reset_concurrency_state() -> None:
"Consider: update implementation plan with correct filenames"
)
)
# Skip delay for stuck subtasks - move to next immediately
continue

# Update status
status_manager.update(state=BuildState.ERROR)
Expand Down Expand Up @@ -1286,6 +1297,7 @@ def _reset_concurrency_state() -> None:
recovery_manager.mark_subtask_stuck(
subtask_id, f"Failed after {attempt_count} attempts"
)
stuck_subtask_ids_local.add(subtask_id)
emit_phase(
ExecutionPhase.FAILED,
f"Subtask {subtask_id} stuck after {attempt_count} attempts",
Expand Down Expand Up @@ -1409,6 +1421,7 @@ def _reset_concurrency_state() -> None:
subtask_id,
f"Tool concurrency errors after {consecutive_concurrency_errors} retries",
)
stuck_subtask_ids_local.add(subtask_id)
print_status(f"Subtask {subtask_id} marked as STUCK", "error")

status_manager.update(state=BuildState.ERROR)
Expand Down
16 changes: 14 additions & 2 deletions apps/backend/cli/batch_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

import json
import re
import shutil
import subprocess
from pathlib import Path
Expand Down Expand Up @@ -60,8 +61,19 @@ def handle_batch_create_command(batch_file: str, project_dir: str) -> bool:
for idx, task in enumerate(tasks, 1):
spec_id = f"{next_id:03d}"
task_title = task.get("title", f"Task {idx}")
task_slug = task_title.lower().replace(" ", "-")[:50]
spec_name = f"{spec_id}-{task_slug}"

# Extract category tag like [sec-001] from title if present
tag_match = re.match(r"^\[(\w+-\d+)\]\s*(.*)", task_title)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Tag regex only matches a single word-digits segment.

r"^\[(\w+-\d+)\]\s*(.*)" won't match tags with multiple segments (e.g., [sec-sub-001]), purely alphabetic tags (e.g., [hotfix]), or tags with dots/slashes. If the tag taxonomy may expand, consider a more permissive capture like r"^\[([\w\-]+)\]\s*(.*)".

🤖 Prompt for AI Agents
In `@apps/backend/cli/batch_commands.py` at line 66, The current regex assigned to
tag_match only captures a single word-digits segment and will miss multi-part or
alphabetic-only tags; update the pattern used where tag_match is created (the
assignment using task_title) to a more permissive capture that allows letters,
digits, hyphens and optionally dots/slashes (e.g., change the regex to capture
one or more of those characters inside the brackets) so tags like [sec-sub-001],
[hotfix], or tags with dots/slashes are matched and the remaining title group is
still captured.

if tag_match:
tag = tag_match.group(1) # e.g. "sec-001"
title_rest = tag_match.group(2) # e.g. "Remove hardcoded API key..."
title_slug = re.sub(r"[^\w\-]", "-", title_rest.lower())
title_slug = re.sub(r"-+", "-", title_slug).strip("-")[:50]
spec_name = f"{spec_id}-[{tag}]-{title_slug}"
Comment on lines 66 to 72
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Brackets in directory names can break shell globbing and downstream tooling.

spec_name like 001-[sec-001]-remove-hardcoded contains literal [ and ], which are glob metacharacters. Any unquoted shell expansion (e.g., ls .auto-claude/specs/001-*) or tools that interpret glob patterns will misinterpret these characters. This also affects Line 56's iterdir()-based ID extraction if future code passes these paths through shell commands or subprocess without quoting.

Consider escaping or replacing brackets in the spec name:

♻️ Suggested fix — use parentheses or plain hyphens instead of brackets
-            spec_name = f"{spec_id}-[{tag}]-{title_slug}"
+            spec_name = f"{spec_id}-{tag}-{title_slug}"

This keeps the tag visible in the directory name without introducing glob-sensitive characters. If visual grouping of the tag is desired, parentheses (sec-001) are safer but still non-ideal; plain hyphens are the most portable.

🤖 Prompt for AI Agents
In `@apps/backend/cli/batch_commands.py` around lines 66 - 72, The generated
directory name spec_name currently embeds literal brackets (constructed in the
block using tag_match, tag, title_slug and spec_id) which are glob
metacharacters; change the formatting to avoid '[' and ']' (e.g., build
spec_name as f"{spec_id}-{tag}-{title_slug}" or
f"{spec_id}-({tag})-{title_slug}" or replace brackets with hyphens) and keep the
existing slug sanitization for title_slug; also review any code that extracts
IDs via iterdir() (the earlier ID-extraction logic) to tolerate the new naming
scheme so directory parsing still works.

else:
task_slug = re.sub(r"[^\w\-]", "-", task_title.lower())
task_slug = re.sub(r"-+", "-", task_slug).strip("-")[:50]
spec_name = f"{spec_id}-{task_slug}"
spec_dir = specs_dir / spec_name
spec_dir.mkdir(exist_ok=True)

Expand Down
5 changes: 5 additions & 0 deletions apps/backend/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ def parse_args() -> argparse.Namespace:

def main() -> None:
"""Main CLI entry point."""
# Preflight self-healing checks (token refresh, ollama, stale locks)
from preflight_hook import run_preflight

run_preflight()

# Set up environment first
setup_environment()

Expand Down
25 changes: 25 additions & 0 deletions apps/backend/core/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,31 @@ def invalidate_project_cache(project_dir: Path | None = None) -> None:
)
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
from claude_agent_sdk.types import HookMatcher

# Monkey-patch the SDK message parser to gracefully handle unknown message types
# (e.g. rate_limit_event) instead of raising MessageParseError and killing agent sessions.
# This is needed until the upstream SDK adds native support for these message types.
# Guarded with try/except so tests with mocked SDK don't break.
try:
from claude_agent_sdk._errors import MessageParseError as _MessageParseError
from claude_agent_sdk._internal import message_parser as _sdk_message_parser
from claude_agent_sdk.types import SystemMessage

_original_parse_message = _sdk_message_parser.parse_message

def _patched_parse_message(data):
try:
return _original_parse_message(data)
except _MessageParseError as e:
if "Unknown message type" in str(e):
msg_type = data.get("type", "unknown")
logger.debug(f"Skipping unknown SDK message type: {msg_type}")
return SystemMessage(subtype=msg_type, data=data)
raise

_sdk_message_parser.parse_message = _patched_parse_message
except (ImportError, AttributeError):
pass # SDK internals not available (e.g. test mocks)
from core.auth import (
configure_sdk_authentication,
get_sdk_env_vars,
Expand Down
6 changes: 5 additions & 1 deletion apps/backend/core/worktree.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,11 @@ def get_worktree_path(self, spec_name: str) -> Path:

def get_branch_name(self, spec_name: str) -> str:
"""Get the branch name for a spec."""
return f"auto-claude/{spec_name}"
# Sanitize spec_name: remove characters invalid in git branch names
sanitized = re.sub(r"[\[\]~^:?*\\{}]", "", spec_name)
# Collapse repeated dashes from removal
sanitized = re.sub(r"-{2,}", "-", sanitized)
return f"auto-claude/{sanitized}"
Comment on lines 345 to 351
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

# Find all calls to get_branch_name
rg -n 'get_branch_name\(' --type=py -B2 -A2

Repository: AndyMik90/Auto-Claude

Length of output: 2309


🏁 Script executed:

# Check for any validation of spec_name before it reaches this function
rg -n 'spec_name' --type=py -B2 -A2 | head -100

Repository: AndyMik90/Auto-Claude

Length of output: 6249


🏁 Script executed:

# Look for tests of the get_branch_name function
rg -n 'test.*get_branch_name|get_branch_name.*test' --type=py -B1 -A5

Repository: AndyMik90/Auto-Claude

Length of output: 47


🏁 Script executed:

# Examine the worktree.py file around the function
head -360 apps/backend/core/worktree.py | tail -60

Repository: AndyMik90/Auto-Claude

Length of output: 2603


🏁 Script executed:

# Find _generate_spec_name implementation
rg -n '_generate_spec_name' --type=py -A10 | head -60

Repository: AndyMik90/Auto-Claude

Length of output: 4293


🏁 Script executed:

# Check the full test_create_worktree_with_spec_name test
rg -n 'test_create_worktree_with_spec_name' --type=py -A20

Repository: AndyMik90/Auto-Claude

Length of output: 1429


🏁 Script executed:

# Look for any validation of spec_name in the codebase
rg -n 'spec_name.*validate|validate.*spec_name|spec_name.*check' --type=py -B2 -A2

Repository: AndyMik90/Auto-Claude

Length of output: 47


🏁 Script executed:

# Find the actual implementation of _generate_spec_name
rg -n 'def _generate_spec_name' --type=py -A30

Repository: AndyMik90/Auto-Claude

Length of output: 2753


🏁 Script executed:

# Check if spec_name can come from external sources (not just _generate_spec_name)
rg -n 'spec_name\s*=' --type=py | grep -v test | head -30

Repository: AndyMik90/Auto-Claude

Length of output: 1751


🏁 Script executed:

# Find the actual generate_spec_name implementation
rg -n 'def generate_spec_name' --type=py -A20

Repository: AndyMik90/Auto-Claude

Length of output: 1360


🏁 Script executed:

# Check batch_commands.py line 64 to see spec_name construction
sed -n '60,70p' apps/backend/cli/batch_commands.py

Repository: AndyMik90/Auto-Claude

Length of output: 511


🏁 Script executed:

# Get the complete generate_spec_name function
sed -n '123,180p' apps/backend/spec/pipeline/models.py

Repository: AndyMik90/Auto-Claude

Length of output: 1080


🏁 Script executed:

# Get more of the generate_spec_name function including the actual sanitization logic
sed -n '123,200p' apps/backend/spec/pipeline/models.py

Repository: AndyMik90/Auto-Claude

Length of output: 1552


🏁 Script executed:

# Get the complete generate_spec_name function
sed -n '123,210p' apps/backend/spec/pipeline/models.py

Repository: AndyMik90/Auto-Claude

Length of output: 1837


Branch name sanitization is technically incomplete but mitigated by upstream validation.

Git's branch naming rules forbid spaces, .. sequences, @{, trailing . or .lock, and control characters. However, all spec_name values reaching this function are pre-sanitized by generate_spec_name() in models.py, which strips non-alphanumeric characters (keeping only hyphens and alphanumerics). This upstream sanitization ensures that problematic patterns like "foo..bar" or "test.lock" cannot occur in practice.

The current implementation is safe and functional, but adding comprehensive git ref validation here would be good defensive practice—for instance, if spec_name ever comes from external or user-controlled input. If you want to harden this, consider extending the sanitization to handle edge cases (spaces, dots, etc.), or optionally use git check-ref-format --normalize for authoritative validation.

🤖 Prompt for AI Agents
In `@apps/backend/core/worktree.py` around lines 345 - 351, The get_branch_name
method currently only strips a limited set of characters which misses
Git-specific invalid patterns; update get_branch_name to defensively enforce git
ref rules by additionally removing/control-replacing spaces, control characters,
consecutive dots (".."), any "@{" sequence, trailing dots or ".lock", and
collapse multiple hyphens, or alternatively call out to git check-ref-format
--normalize to validate/normalize the sanitized name; ensure you reference
get_branch_name and account for upstream generate_spec_name() but still harden
get_branch_name so it never returns a branch string with forbidden patterns.


def worktree_exists(self, spec_name: str) -> bool:
"""Check if a worktree exists for a spec."""
Expand Down
Loading
Loading