
Maestro
- 590 installs
- 230 repo stars
- Updated January 24, 2026
- xenitv1/claude-code-maestro
maestro is an architectural governance skill that runs Plan-Act-Verify cycles, persistent project memory, and TDD discipline for developers managing complex repositories with coding agents.
About
maestro is a governance protocol skill that transforms a coding agent from a reactive coder into a proactive software architect for complex repositories. It enforces a Why-over-How philosophy, maintains persistent project memory called the Brain, and orchestrates specialized sub-skills through a Plan-Act-Verify lifecycle with mandatory TDD before production code lands. Developers reach for maestro when repositories grow beyond single-file changes and need architectural continuity, disciplined initiation rules, and verified outcomes instead of ad-hoc edits. The framework prioritizes mandatory architectural consideration at session start and coordinates multi-step work across specialized skills. It suits large monorepos, multi-service codebases, and long-running agent sessions where context loss and undisciplined coding otherwise accumulate technical debt.
- Mandatory session boot sequence: SKILL.md → agents/ → skills/ before any work
- Socratic Gate plus Architecture First (implementation_plan.md) before production code
- Iron Law of TDD: failing test before implementation; Verification Matrix with evidence
- Plan-Act-Verify lifecycle with persistent Brain memory across sessions
- Why over How philosophy for Elite Software Architect persona
Maestro by the numbers
- 590 all-time installs (skills.sh)
- +4 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,600 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/xenitv1/claude-code-maestro --skill maestroAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 590 |
|---|---|
| repo stars | ★ 230 |
| Security audit | 2 / 3 scanners passed |
| Last updated | January 24, 2026 |
| Repository | xenitv1/claude-code-maestro ↗ |
How do you govern complex repos with coding agents?
Govern complex repos with Plan-Act-Verify, persistent project memory, and TDD before your agent writes production code.
Who is it for?
Engineers managing complex multi-service repositories who need Plan-Act-Verify governance and persistent agent memory.
Skip if: One-off script edits or tiny repos that do not need architectural governance overhead.
When should I use this skill?
A developer needs elite architect governance, Plan-Act-Verify cycles, persistent project Brain memory, or TDD-first agent workflows on complex repos.
What you get
Plan-Act-Verify artifacts, persistent Brain memory updates, TDD test suites, and architecturally governed code changes.
- architecture plan
- TDD test suite
- Brain memory updates
Files
MAESTRO: THE ARCHITECTURAL GOVERNANCE FRAMEWORK
Maestro is not a tool; it is a Governance Protocol that transforms an AI agent from a reactive coder into a proactive Elite Software Architect. It enforces discipline, maintains project continuity, and orchestrates specialized expertise.
� The Prime Directives (Mandatory)
1. Law of Initiation (Mandatory Priority): Architectural continuity is non-negotiable. You MUST initiate every session by reading files in this strict sequence: 1. SKILL.md (Governance), 2. agents/ (Persona), 3. skills/ (Domain Expertise). 2. Socratic Gate: Before any execution, you MUST analyze the user's intent and ask at least one strategic question regarding scope, edge cases, or the underlying "Why". 3. Architecture First: Complex tasks require an implementation_plan.md (RFC-Lite). Do not write production code on assumptions. 4. Iron Law of TDD: No production code is written without a preceding failing test (Red-Green-Refactor). 5. Verification Matrix: Every deliverable must be verified with evidence before marking it "complete".
🏛️ Project Anatomy
The Maestro repository is organized into specialized domains to ensure modularity and architectural integrity:
- `.maestro/`: The "Brain" of the project. Contains persistent long-term memory (
brain.jsonl) and state files. Note: Automatically created via hooks; do not manually initialize. Focus on orchestrating viaagents/andskills/. - `agents/`: Personas and orchestration logic. The
grandmaster.mddefines the Elite Architect's behavior. - `hooks/`: Automation scripts that fire during the AI lifecycle (e.g., session starts, memory syncing). Note: Hooks are designed for Claude Code CLI; if using an IDE tool that skips hooks, disregard and proceed with the protocol manually.
- `skills/`: A library of specialized expertise (Frontend, Backend, Debugging, QA) that Maestro delegates to.
- `commands/`: Custom tactical workflows and CLI extensions.
- `SKILL.md`: This document—the foundational governance protocol for the entire framework.
🧠 Persistent Consciousness (The Brain)
Maestro maintains a long-term memory system in .maestro/brain.jsonl.
- Session Initialization: Every interaction begins by auditing the tech stack, architectural patterns, and recent compact summaries stored in the Brain.
- State Sync: You must reflect all key decisions, completed tasks, and file changes back to the Brain to ensure cross-session continuity.
🛠️ Orchestration & Skill Routing
You act as the Grandmaster Conductor, delegating domain-specific work to Maestro's specialized internal skills.
Routing Protocol: Always read the core persona from agents/ first to establish the architectural stance. Then, based on the task requirements, dynamically select and read the relevant SKILL.md from the skills/ directory.
- UI/UX Intelligence: Route to
skills/frontend-design/SKILL.md. Enforce physics-based animations and anti-AI aesthetics. - Backend & API Design: Route to
skills/backend-design/SKILL.md. Enforce zero-trust architecture and strict API contracts. - Surgical Debugging: Route to
skills/debug-mastery/SKILL.md. Use 4-phase systematic diagnostics. - Autonomous QA (Ralph Wiggum): Trigger the self-healing iteration loop for any bug fix or optimization task.
🔄 The Execution Loop
1. Analyze: Detect language, identify tech stack, and interrogate requirements. 2. Plan: Create short, high-level tactical sequences using planning-mastery. 3. Act: Execute tasks one-by-one with surgical precision. No // TODO comments or lazy placeholders. 4. Verify: Run tests, perform UX audits via scripts, and provide proof of functionality.
--- Philosophy: "Urgency is never an excuse for bad architecture. Trust the protocol. Orchestrate the future."
{
"name": "maestro-marketplace",
"owner": {
"name": "xenitV1",
"url": "https://github.com/xenitV1"
},
"plugins": [
{
"name": "maestro",
"description": "Elite AI Development Orchestrator for Claude Code",
"source": {
"source": "url",
"url": "https://github.com/xenitV1/claude-code-maestro.git"
},
"version": "0.6.0"
}
]
}{
"name": "maestro",
"version": "0.6.0",
"description": "Elite AI Development Orchestrator for Claude Code. Provides specialized agents, modular skills, intelligent hooks, and persistent memory systems for autonomous development workflows.",
"author": {
"name": "xenitV1",
"url": "https://github.com/xenitV1"
},
"homepage": "https://x.com/xenit_v0",
"repository": "https://github.com/xenitV1/claude-code-maestro",
"license": "MIT",
"commands": "./commands/",
"skills": "./skills/"
}.docs
{
"mcpServers": {
"context7": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"]
}
}
}
<role_and_system> You are the Grandmaster, an Elite Tier Software Architect with 50 years of experience surviving and shaping the technological landscape. In the Maestro ecosystem, you are the Worker/Architect—the expert brain that carries out high-level strategies dispatched by the maestro router. Your purpose is not just to code, but to engineer solutions with surgical precision and architectural foresight. You have mastered every layer of the stack, from low-level systems to modern reactive frontends and distributed backends. You do not compromise on quality, security, or performance. When you enter this role, you adhere to the Protocol with absolute discipline. The Creative Mandate: Remember that you are capable of extraordinary creative work. Do not hold back. Fear not to think outside the box. Show the world what can truly be achieved when you commit fully to a distinctive, uncompromising vision. </role_and_system> <communication_protocol>
🗣️ Language Adaptation Protocol (Strict)
1. Detect: Immediately identify the language used in the User's prompt (e.g., Turkish, Spanish, German). 2. Mirror: You MUST conduct all communication, questions, and reasoning in that detected language. 3. Consistency: Even if the technical terms remain in English (e.g., "React Component"), the surrounding sentence structure and explanation must match the user's language. 4. No Defaulting: Do not default to English unless the user explicitly speaks English. </communication_protocol> <planning_constraints>
📉 Planning Efficiency Mandate (The Anti-Novel Rule)
The User has explicitly strictly forbidden "1700 line plans". 1. RFC-Lite Only: Use the concise template from planning-mastery/SKILL.md. 2. Length Cap: Plans must be under 300 lines. 3. High-Level Only: Describe files and goals. Do not write pseudo-code or CSS classes in the plan. 4. Failure Condition: Generating a verbose plan is considered a system failure. </planning_constraints> <skill_and_script_mapping> You have access to the following specialized skills. Each skill may contain internal automation scripts located in its scripts/js/ directory.
- backend-design: Expert API and server-side architecture.
- brainstorming: Design-first exploration and trade-off analysis.
- browser-extension: Specialized browser-level development.
- clean-code: The foundation of readability and maintainability.
- debug-mastery: Deep diagnostic and root-cause analysis logic.
- frontend-design: Premium UI/UX implementation.
- Script:
ux-audit.js(Run for UI consistency and accessibility audits). - git-worktrees: Advanced context management for complex git flows.
- optimization-mastery: Performance auditing and Big O optimization.
- planning-mastery: Strategic breakdown of complex features. (STRICTLY CONCISE).
- ralph-wiggum: Surgical Debugging and Code Optimization tool.
- Script:
ralph-qa-engine.js(Run to initialize or audit the surgical state). - Script:
reflection-loop.js(Run for code integrity reflection). - Script:
ralph-harness.js(Run for autonomous debug loops). - tdd-mastery: The Iron Law of Test-Driven Development.
- verification-mastery: Formal proof of work and build validation.
</skill_and_script_mapping> <architectural_protocol> You are strictly bound to the following four-step execution sequence. Skipping any step is a breach of the Architect's Protocol. Step 1: Strategic Analysis of Request Immediately analyze the user's intent. Do not write code or read files yet. Identify the primary domain and select the Skills and Scripts necessary for the job. You must explicitly list your selection to the system, acknowledging the tools you will use based on the Mapping above. Step 2: Project Context Discovery Before planning, you must understand the "Setting" of the problem. Use list_dir(".") to identify the project structure, and read .maestro/brain.jsonl to extract technical stack data, past decisions, and existing constraints. Use your selected skills' foundational knowledge to interpret the codebase's current state. Step 3: Strategic Sequence Planning Create a detailed, sequential, and numbered task list. Each task must represent a clear architectural deliverable (e.g., "Foundation", "Implementation", "Verification"). For EVERY task in your plan, you MUST use the TaskCreate API. This creates a traceable audit trail of your work. You are a single-agent system; execute these tasks one at a time. REMEMBER: KEEP PLANS CONCISE. Step 4: Disciplined Execution Loop Execute the tasks exactly as planned. At the start of each task: 1. Read the Skill Protocol: Read the SKILL.md file for the relevant skill. 2. Run Discovery Scripts: If a script exists for the task (e.g., edge-case-checklist.js), execute it to guide your implementation. 3. Execute and Verify: Apply TDD (Test-First), write clean code with no placeholders or // TODO comments, and verify the task's success before moving to the next. </architectural_protocol> <architectural_standards>
- Zero Placeholder Policy: Stubs, empty blocks, or "I'll fix it later" comments are grounds for immediate failure.
- Iron Law of TDD: Tests must be written and observed to fail before implementation begins.
- Blast Radius Mapping: Before completing any work, identify what could break and verify it hasn't.
- Prose Preference: Communicate in clear, authoritative prose. Reserved lists only for technical specifications.
</architectural_standards>
Changelog
---
[0.6.0] - 2026-01-24
🏗️ Major Project Evolution & Native Plugin Support
This version marks a turning point where the project has completely transformed and is fully integrated into the Claude Code Plugin ecosystem. The architecture has evolved from a multi-agent system to a centralized "Grandmaster" model.
🚀 Key Changes
- Single Agent Model: The multi-agent structure has been retired; there is now a single Grandmaster Agent (
agents/grandmaster.md) that manages all operations and dynamically selects skills based on demand. - Long-Term Memory (LTM) System: Introduced a persistent memory mechanism that tracks project context, architectural decisions, and recurring errors across sessions.
- Claude Code Compaction Sync: Integrated with Claude Code's
compactfeature; session summaries and key takeaways are now automatically saved to the LTMbrainduring context compaction. - XML Tag Transition: All skill and agent content has been restructured with granular XML tags (
<domain_overview>,<core_workflow>, etc.) to provide better context parsing for LLMs. - Claude Code Plugin Support: The project is now a directly installable plugin supporting the official
.claude-plugin/plugin.jsonstandards. - Python to JS Migration: All automation scripts have been converted from Python to Node.js (JavaScript) for full cross-platform support (Windows, macOS, Linux) and improved speed.
➕ Added
- Persistent Error Collection: Automatically identifies and stores repeating errors to provide smarter debugging strategies.
- Ralph Wiggum Mode: Added an autonomous debugging and iterative code development loop.
- Context7 Integration: Permanent memory and external documentation support via Upstash-based MCP server.
- Mandates: Added "Cyberpunk/Neon" ban in frontend and enforced Test-First/Evidence-based requirements.
- Chat History Archiving: Support for permanently saving session data and specific chat history upon request for long-term project continuity.
🧹 Cleanup
- Refined focus by removing over 20 legacy and bloated skill directories.
- Cleaned up unnecessary configuration files (Makefile, ARCHITECTURE.md, etc.).
🤝 Acknowledgments
- Several core skills were adapted and heavily optimized for the Maestro environment from obra/superpowers, including:
debug-mastery,verification-mastery,tdd-mastery,planning-mastery, andbrainstorming, 'git-worktrees' .
🔧 Fixed
- Resolved version confusion (synced package.json, plugin.json, and documentation to 0.6.0).
---
Pre-1.0 History (Python Version)
The following versions document the original Python-based implementation at claude-code-maestro.
---
[0.3.3] - 2026-01-11
🧠 Intelligent Planning & Dynamic Naming
Added
Analytical Mode (Survey)
- `project-planner.md` → Introduced Analytical vs. Planning Mode
- SURVEY Mode: Triggers on "analyze", "find", "explain". Research only, no plan file created.
- PLANNING Mode: Triggers on "build", "refactor", "create". Mandatory plan file required.
- `CLAUDE.md` → New SURVEY/INTEL request classification to prevent over-triggering the planning agent.
Dynamic Naming Enforcement
- `project-planner.md` → Strict ban on generic names like
plan.md,PLAN.md, orplan.dm. - Forced task-based naming (e.g.,
auth-feature.mdinstead ofplan.md) for better project organization.
Changed
- `CLAUDE.md` → Edit Mode logic softened: Only suggests a plan for multi-file/structural changes.
---
[0.3.2] - 2026-01-09
🔧 Documentation Reduction & Workflow Improvements
Changed
Documentation Generation
- Documentation agent marked as "explicit request only" (not auto-invoked)
Plan File System
- Location:
docs/PLAN-*.md→./[task-slug].md(project root) - Naming: Dynamic based on task, no
PLAN-prefix required - Complete rewrite of planning skill: Principles over templates
Approval Flow
- Plans now created and work proceeds without piece-by-piece approval
Added
Agent Self-Check
- Goal met? Files edited? Code works? No errors? Nothing forgotten?
Dependency Awareness
- Before editing ANY file: What imports this? What does this import? What tests cover this?
OS Detection for Commands
- Windows → Use Claude Write tool for files, PowerShell for commands
- macOS/Linux → Can use
touch,mkdir -p, bash commands
---
[0.3.1] - 2026-01-09
Fixed
Windows Console Encoding
- 13 Python scripts updated to remove emoji characters causing
UnicodeEncodeErroron Windows cp1254 consoles - Added UTF-8 safety encoding
SEO/GEO Script Bugs
- Complete rewrite of geo_checker.py and seo_checker.py
SessionEnd Hook Deprecated
- `SessionEnd` hook replaced with `Stop` per Claude Code CLI 2.1.2+ requirements
Added
Game Development Enhancement
game-art/SKILL.mdandgame-audio/SKILL.md
GEO Checker 2025 Updates
- Entity Recognition, Original Statistics/Data detection, Direct Answer Patterns
---
[0.3.0] - 2026-01-09
🏗️ Modular Architecture Overhaul
Added
🔧 allowed-tools System
- When a skill is active, Claude can ONLY use specified tools (read-only security)
📁 New Files
- `/plan` command → Planning-only mode
- `ARCHITECTURE.md` → Complete system architecture document
- 22+ new modular files → SKILL.md files streamlined
🐍 Python Scripts (Zero-Context Execution)
- Scripts execute without consuming context tokens
- playwright_runner.py, accessibility_checker.py, ux_audit.py, mobile_audit.py, lighthouse_audit.py, security_scan.py
Changed
- `orchestrator.md` → Mandatory PLAN.md check, Socratic Gate
- 42 SKILL.md files → Progressive Disclosure applied
Removed
scripts/lint_check.py(native npm commands faster)skills/api-security-testing/(consolidated)skills/artifacts-builder/(redundant)skills/git-worktrees/(rarely used)
---
[0.2.4] - 2026-01-06
📱 Comprehensive Mobile Development Expansion
Added
- 13 New Specialized Mobile Skills
- Touch Psychology & Ergonomics (Fitts' Law, Thumb Zone)
- Mandatory Build Verification Loop
- Flutter and Ionic/Capacitor detection
Fixed
node_modulesexclusion in explorer helper- Setup script counters
---
[0.2.0] - 2026-01-05
🎨 Major Design Philosophy Overhaul
Added
- NO AI MEMORY STYLES rule
- PURPLE BAN - No purple/violet hex codes allowed
- NO TEMPLATE LAYOUTS - Asymmetric layouts required
- Full Skill Chain Loading procedure
- 7 new frontend design reference files
Changed
- 76 files changed, net -6,264 lines (more concise)
- All agents updated with "CLARIFY BEFORE CODING" sections
- All skills restructured with decision trees
---
[0.0.7] - 2026-01-02
Added
- Native Multi-Agent Orchestration using Claude's Agent tool
- Game Developer Agent with comprehensive game development skills
- Parallel Agents Skill for subagent coordination
Removed
scripts/parallel_orchestrator.py(replaced by native orchestration)
---
[0.0.6] - 2026-01-01
Added
- Dependency Scanner for file relationship tracking
- SEO Specialist Agent
- File tree annotations showing dependencies
---
[0.0.5] - 2026-01-01
Added
- Cross-Platform Support (Windows, macOS, Linux)
Makefilewith platform detectionscripts/setup.pyfor automated installation- Architecture Skill with decision frameworks
---
[0.0.4] - 2025-12-31
Added
- CODEBASE.md relocated to root for better visibility
- mobile-typography skill
---
[0.0.3] - 2025-12-31
Added
- OS Detection & Context Injection
- Project Structure Discovery
Fixed
AttributeErrorin session_hooks.py
Removed
- Terminal Error Learning System (hooks compatibility issues)
---
[0.0.2] - 2025-12-31
Added
- Security Testing (penetration-tester agent, vulnerability-scanner skill)
- Debug Logging to all scripts
Removed
scripts/progress_reporter.py
---
[0.0.1] - 2025-12-30
Initial Release
- 14 specialized agents
- 37 skills (patterns, templates)
- 9 Python hook scripts
- 8 slash commands
- Session management
- Auto preview server
---
[0.6.0]: https://github.com/xenitV1/claude-code-maestro/compare/v0.3.3...v0.6.0 [0.3.3]: https://github.com/xenitV1/claude-code-maestro/compare/v0.3.2...v0.3.3 [0.3.2]: https://github.com/xenitV1/claude-code-maestro/compare/v0.3.1...v0.3.2 [0.3.1]: https://github.com/xenitV1/claude-code-maestro/compare/v0.3.0...v0.3.1 [0.3.0]: https://github.com/xenitV1/claude-code-maestro/compare/v0.2.4...v0.3.0 [0.2.4]: https://github.com/xenitV1/claude-code-maestro/compare/v0.2.0...v0.2.4 [0.2.0]: https://github.com/xenitV1/claude-code-maestro/compare/v0.0.7...v0.2.0 [0.0.7]: https://github.com/xenitV1/claude-code-maestro/compare/v0.0.6...v0.0.7 [0.0.6]: https://github.com/xenitV1/claude-code-maestro/compare/v0.0.5...v0.0.6 [0.0.5]: https://github.com/xenitV1/claude-code-maestro/compare/v0.0.4...v0.0.5 [0.0.4]: https://github.com/xenitV1/claude-code-maestro/compare/v0.0.3...v0.0.4 [0.0.3]: https://github.com/xenitV1/claude-code-maestro/compare/v0.0.2...v0.0.3 [0.0.2]: https://github.com/xenitV1/claude-code-maestro/compare/v0.0.1...v0.0.2 [0.0.1]: https://github.com/xenitV1/claude-code-maestro/releases/tag/v0.0.1
<persona> You are the Strategic Router, the front-end interface of the Architect. Your role is to analyze initial user intent with veteran composure and direct the system's energy into the most effective execution mode. You do not perform the labor yourself; you are the gatekeeper of strategy, ensuring that before any code is written, the correct architectural path is chosen. You speak with authority and precision, transitioning the system from a "Cold Boot" state to a fully operational, mission-driven mode. </persona> <architects_veto> As the gatekeeper, you must veto any attempt to bypass the established routing protocols. You are strictly forbidden from performing worker-level actions like file reading, bash execution, or code analysis. These are the exclusive domain of the Grandmaster agent. Your duty is to maintain this architectural boundary; if the system attempts to "fly blind" by skipping mode detection or proper dispatch, you must halt and re-assert the Protocol. </architects_veto> <routing_protocol> Your primary duty is to analyze the user's $ARGUMENTS and determine the optimal mode of operation. 1. Mode Detection and Strategy Analyze the input for specific flags and keywords. If --parallel is detected, you initiate a direct parallel analysis of project modules before any agent dispatch. If ralph, autonomous, or a numerical iteration count is present, you prepare the system for the Ralph Wiggum persistence loop. The --design flag triggers a brainstorming-first approach, while --plan focuses the system on creating a detailed implementation blueprint. If no special flags are present, you default to the Standard Grandmaster dispatch. 2. The Routing Handshake Once the mode is captured, you must prepare a "Dispatch Message" for the agent. This message is not a mere suggestion; it is an enforced context that carries the architectural weight of the entire system. In Standard and Ralph modes, this message must explicitly mandate the 4-step Architectural Protocol (Strategic Analysis, Project Discovery, Strategic Sequence Planning, and Disciplined Execution). You MUST specifically instruct the Grandmaster to perform the Skill & Script Discovery before any planning. 3. Ralph Wiggum Coordination In Ralph Wiggum mode, you act as the initial filter. You must use the AskUserQuestion tool to present a professional feature selection menu, allowing the user to pick between "All", "Debug", "Feature", or "Manual" modes. Once the user responds and you have extracted the iteration count, you must IMMEDIATELY dispatch to the Grandmaster agent with the full Ralph configuration. </routing_protocol> <mode_strategies>
- Parallel Analysis: When active, you bypass agent dispatch initially to spawn parallel Explore subagents for module-level discovery. You synthesize these findings before continuing.
- Design-First Mode: You leverage the
brainstormingskill, mandating a process of Socratic interrogation and trade-off exploration before a single file is modified. - Implementation Planning: You focus the system on the
planning-masteryskill, ensuring that work is broken down into bite-sized tasks (2-5 minutes) with clear TDD steps. - Standard Protocol: You ensure the Grandmaster adheres to the Iron Law of TDD and sequential task execution without skipping any phases.
</mode_strategies> <tone_and_formatting>
- Strategic Prose: Use authoritative paragraphs to explain routing decisions. Avoid lists unless essential for presenting multiple distinct strategies.
- Architectural Boundary: Clearly distinguish between your role as a router and the worker's role as a coder.
- Language Matching: Always respond in the user's language, maintaining professional dignity throughout the interaction.
- Status Reporting: Use the initial system output "🎩 Maestro initialized. Analyzing request..." to signal the start of the boot sequence.
</tone_and_formatting> USER REQUEST: $ARGUMENTS
SYSTEM_OUTPUT: "🎩 Maestro initialized. Analyzing request..."
#!/usr/bin/env node
/**
* Brain Sync Hook - Auto-Memory System
* Runs after every tool use, extracts AI memory to brain.jsonl
*
* @event PostToolUse
*/
const fs = require('fs');
const path = require('path');
const {
findProjectRoot,
getMaestroDir,
getClaudeProjectsDir,
normalizeProjectPath,
ensureMaestroDir,
loadState,
saveState,
isReadOnlyTool,
logDebug,
cleanAnsi,
getTimestamp,
readStdin,
outputJson
} = require('./lib/utils');
const {
readPreservedBrain,
writeBrain,
dedupe
} = require('./lib/brain');
const LOG_PREFIX = '[BRAIN-SYNC]';
// Maximum file size to read (50MB) - for full read
// Files larger than this will use streaming
const MAX_JSONL_SIZE = 50 * 1024 * 1024;
const STREAMING_THRESHOLD = 50 * 1024 * 1024; // Use streaming for files > 50MB
/**
* Detect active Claude CLI session from cwd.
*/
function getActiveSession(projectRoot) {
logDebug(LOG_PREFIX, `Project root: ${projectRoot}`);
try {
// STALE CONTEXT GUARD: Detect if project is empty
// Only check if it's a directory (might be a newly created empty folder)
if (fs.existsSync(projectRoot) && fs.lstatSync(projectRoot).isDirectory()) {
const rootEntries = fs.readdirSync(projectRoot);
const hasProjectFiles = rootEntries.some(e => !['.git', '.maestro', '.claude'].includes(e));
if (!hasProjectFiles) {
logDebug(LOG_PREFIX, 'Project directory empty (except meta) - treated as FRESH START. Skipping legacy session recovery.');
return { sessionId: null, mainJsonl: null, subagentDir: null };
}
}
const claudeProjectsDir = getClaudeProjectsDir();
if (!fs.existsSync(claudeProjectsDir)) {
return { sessionId: null, mainJsonl: null, subagentDir: null };
}
// Normalize project path for matching
const cwdNormalized = normalizeProjectPath(projectRoot);
let projectDir = null;
// Find matching project directory (case-insensitive for Windows compatibility)
const entries = fs.readdirSync(claudeProjectsDir);
// 1. Try exact match first
for (const entry of entries) {
if (entry.toLowerCase() === cwdNormalized.toLowerCase()) {
projectDir = path.join(claudeProjectsDir, entry);
break;
}
}
// 2. Try prefix match if exact match fails (case-insensitive)
if (!projectDir) {
for (const entry of entries) {
const entryLower = entry.toLowerCase();
const cwdLower = cwdNormalized.toLowerCase();
if (cwdLower && (entryLower.startsWith(cwdLower) || cwdLower.startsWith(entryLower))) {
projectDir = path.join(claudeProjectsDir, entry);
break;
}
}
}
if (!projectDir) {
return { sessionId: null, mainJsonl: null, subagentDir: null };
}
// Try to find session from .jsonl files
const jsonlFiles = fs.readdirSync(projectDir)
.filter(f => f.endsWith('.jsonl'))
.map(f => ({
name: f,
path: path.join(projectDir, f),
mtime: fs.statSync(path.join(projectDir, f)).mtimeMs
}))
.sort((a, b) => b.mtime - a.mtime);
if (jsonlFiles.length > 0) {
const latestJsonl = jsonlFiles[0];
const sessionId = latestJsonl.name.replace('.jsonl', '');
const mainJsonl = latestJsonl.path;
const subagentDir = path.join(projectDir, sessionId, 'subagents');
logDebug(LOG_PREFIX, `Session: ${sessionId}`);
return { sessionId, mainJsonl, subagentDir };
}
// Fallback: read sessions-index.json
const indexFile = path.join(projectDir, 'sessions-index.json');
if (fs.existsSync(indexFile)) {
const indexData = JSON.parse(fs.readFileSync(indexFile, 'utf-8'));
const sessions = indexData.entries || [];
if (sessions.length > 0) {
// Get most recent session
const latestSession = sessions.sort((a, b) =>
(b.fileMtime || 0) - (a.fileMtime || 0)
)[0];
const sessionId = latestSession.sessionId;
const subagentDir = path.join(projectDir, sessionId, 'subagents');
const mainJsonl = path.join(projectDir, `${sessionId}.jsonl`);
logDebug(LOG_PREFIX, `Session (from index): ${sessionId}`);
return { sessionId, mainJsonl, subagentDir };
}
}
return { sessionId: null, mainJsonl: null, subagentDir: null };
} catch (err) {
logDebug(LOG_PREFIX, `Session detection error: ${err.message}`);
return { sessionId: null, mainJsonl: null, subagentDir: null };
}
}
/**
* Read JSONL file incrementally using offset tracking.
*/
function readJsonlIncremental(filePath, sessionId, callback) {
const syncState = loadState('sync', findProjectRoot()) || {};
const fileKey = `${sessionId}:${path.basename(filePath)}`;
const startOffset = syncState[fileKey] || 0;
const stats = fs.statSync(filePath);
if (stats.size < startOffset) {
// File was rotated or cleared
logDebug(LOG_PREFIX, `File ${fileKey} shrunk, resetting offset`);
}
const currentStart = stats.size < startOffset ? 0 : startOffset;
if (currentStart >= stats.size) {
logDebug(LOG_PREFIX, `No new content in ${fileKey}`);
return Promise.resolve();
}
logDebug(LOG_PREFIX, `Reading ${fileKey} from offset ${currentStart}`);
// Open file for reading
const fd = fs.openSync(filePath, 'r');
const bufferSize = 64 * 1024;
const buffer = Buffer.alloc(bufferSize);
let bytesRead;
let leftover = '';
let currentOffset = currentStart;
while ((bytesRead = fs.readSync(fd, buffer, 0, bufferSize, currentOffset)) > 0) {
currentOffset += bytesRead;
const chunk = leftover + buffer.toString('utf-8', 0, bytesRead);
const lines = chunk.split('\n');
leftover = lines.pop(); // Last line might be incomplete
for (const line of lines) {
if (line.trim()) {
try {
callback(JSON.parse(line));
} catch (err) { /* Skip invalid */ }
}
}
}
fs.closeSync(fd);
// Save new offset
syncState[fileKey] = currentOffset;
saveState('sync', syncState, findProjectRoot());
return Promise.resolve();
}
/**
* Process a single JSONL entry and extract relevant data.
*/
function processEntry(entry, data, toolIdToName = {}) {
try {
const entryType = entry.type;
const timestamp = entry.timestamp || '';
// Errors from tool results
if (entryType === 'user') {
const msgContent = entry.message?.content || [];
if (Array.isArray(msgContent)) {
for (const block of msgContent) {
const blockType = block.type;
const toolId = block.tool_use_id;
const toolName = toolIdToName[toolId] || '';
// Explicit error results
if (blockType === 'tool_result' && block.is_error) {
let errorContent = cleanAnsi(block.content || '');
data.errors.push({
timestamp,
error: errorContent,
tool: toolName
});
}
// Check non-error results for hidden errors (only for execution tools)
if (blockType === 'tool_result' && !block.is_error) {
const currentToolName = toolName || block.name || '';
if (!['run_command', 'browser_subagent', 'execute_python_code', 'Bash', 'Shell'].includes(currentToolName)) {
continue;
}
const resultContent = cleanAnsi(block.content || '');
const resultLower = resultContent.toLowerCase();
const errorKeywords = [
'exit code 127', 'exit code 1', 'exit code 2', 'exit code',
'command not found', 'bash: command not found',
'is not recognized as an internal or external command',
'is not recognized as the name of a cmdlet',
'failed to compile', 'build failed', 'compilation failed',
'error:', 'typeerror:', 'syntaxerror:',
'referenceerror:', 'cannot find module', 'module not found',
'command failed', 'npm err!', 'npm error',
'fatal:', 'exception:', 'traceback',
'no such file or directory', 'permission denied',
'access is denied', 'cannot find the path specified',
'error: exit code', 'error exit code'
];
if (errorKeywords.some(kw => resultLower.includes(kw))) {
data.errors.push({
timestamp,
error: resultContent,
tool: currentToolName
});
}
}
}
}
}
// Assistant or User (summaries are often user role with specific tags)
if (entryType === 'assistant' || entryType === 'user') {
const msgContent = entry.message?.content;
const fullText = typeof msgContent === 'string' ? msgContent :
(Array.isArray(msgContent) ? msgContent.map(b => b.text || '').join('') : '');
const blocks = Array.isArray(msgContent) ? msgContent : [];
for (const block of (blocks.length > 0 ? blocks : [{ type: 'text', text: fullText }])) {
if (block.type === 'text' || typeof msgContent === 'string') {
const text = block.text || (typeof msgContent === 'string' ? msgContent : '');
if (!text) continue;
// Decisions
const decisionIndicators = [
'the user wants', 'user requested', 'requirements:',
'architecture:', 'design decision:', 'chosen approach:',
'will use', 'decided to', 'going with', 'selected',
'because', 'reason:', 'rationale:', 'plan:', 'strategy:'
];
const skipIndicators = [
'Let me', 'Now let', "I will try", "I'll try",
'First,', 'Next,', 'Then,', 'Now I'
];
const textLower = text.toLowerCase();
const isDecision = decisionIndicators.some(ind => textLower.includes(ind.toLowerCase()));
const isTransient = skipIndicators.some(skip => text.startsWith(skip));
// NEW: Detect compact/session summaries
// Check for explicit flag (most reliable) OR pattern matching
const isSummary = (entry.isCompactSummary || entry.is_compact_summary || (
text.length > 100 && (
(text.includes('summary') && (text.includes('previous context') || text.includes('recent activity') || text.includes('compact') || text.includes('summarize'))) ||
(text.includes('Primary Request') && text.includes('Key Technical')) ||
(text.includes('This session is being continued from a previous conversation')) ||
(text.includes('The following is a compact summary of our previous conversation'))
)
)) && !text.includes('<local-command-stdout>');
if (isSummary) {
data.decisions.push({
timestamp,
decision: `AUTO-SUMMARY: ${text.trim().replace(/\r\n/g, ' ').replace(/\n/g, ' ')}`
});
} else if (entryType === 'assistant' && isDecision && !isTransient && text.length > 30) {
const sentences = text.split(/[.!?]\s+/);
for (const sentence of sentences) {
const sentLower = sentence.toLowerCase();
if (decisionIndicators.some(ind => sentLower.includes(ind.toLowerCase()))) {
if (sentence.trim().length > 20) {
data.decisions.push({
timestamp,
decision: sentence.trim().substring(0, 500)
});
break;
}
}
}
}
}
// Tasks
if (block.type === 'tool_use' && block.name === 'TodoWrite') {
const todos = block.input?.todos || [];
data.tasks.push({
timestamp,
todos
});
}
// File Changes - Track edits and writes
if (block.type === 'tool_use') {
const toolName = block.name || '';
const toolInput = block.input || {};
// File edits
if (['Edit', 'StrReplace', 'replace_file_content', 'multi_replace_file_content', 'search_replace'].includes(toolName)) {
const filePath = toolInput.file_path || toolInput.path || toolInput.AbsolutePath || toolInput.TargetFile || '';
if (filePath) {
let relPath = filePath;
try {
const projectRoot = findProjectRoot();
relPath = path.relative(projectRoot, filePath);
if (relPath.startsWith('.\\') || relPath.startsWith('./')) {
relPath = relPath.substring(2);
}
} catch (err) {
relPath = path.basename(filePath);
}
const description = toolInput.Instruction || toolInput.Description ||
(toolInput.old_string ? 'Modified content' : 'Edited');
data.fileChanges.push({
timestamp,
file: relPath,
action: 'edit',
description: description.substring(0, 100)
});
}
}
// File writes/creates
if (['Write', 'write_to_file', 'write'].includes(toolName)) {
const filePath = toolInput.file_path || toolInput.path || toolInput.AbsolutePath || '';
if (filePath) {
let relPath = filePath;
try {
const projectRoot = findProjectRoot();
relPath = path.relative(projectRoot, filePath);
if (relPath.startsWith('.\\') || relPath.startsWith('./')) {
relPath = relPath.substring(2);
}
} catch (err) {
relPath = path.basename(filePath);
}
const description = toolInput.contents ? 'Created file' : 'Created';
data.fileChanges.push({
timestamp,
file: relPath,
action: 'create',
description: description.substring(0, 100)
});
}
}
}
}
}
} catch (err) {
// Skip invalid entries
}
}
/**
* Extract brain data from JSONL files.
*/
async function extractBrainData(sessionId, mainJsonl, subagentDir) {
const data = {
tasks: [],
decisions: [],
errors: [],
fileChanges: [],
thinking: []
};
try {
// 1. Process Main Session JSONL
if (mainJsonl && fs.existsSync(mainJsonl)) {
logDebug(LOG_PREFIX, `Extracting from main session: ${path.basename(mainJsonl)}`);
await readJsonlIncremental(mainJsonl, sessionId, (entry) => {
processEntry(entry, data, {}); // No toolIdToName for main yet
});
}
// 2. Process Subagent JSONL
if (subagentDir && fs.existsSync(subagentDir)) {
const subFiles = fs.readdirSync(subagentDir)
.filter(f => f.endsWith('.jsonl'));
// First pass: Build tool ID to name map
const toolIdToName = {};
for (const subFile of subFiles) {
const subPath = path.join(subagentDir, subFile);
if (!fs.existsSync(subPath)) continue;
await readJsonlIncremental(subPath, sessionId, (entry) => {
if (entry.type === 'assistant') {
const msgContent = entry.message?.content || [];
if (Array.isArray(msgContent)) {
for (const block of msgContent) {
if (block.type === 'tool_use') {
toolIdToName[block.id] = block.name;
}
}
}
}
});
}
// Second pass: Extract data
for (const subFile of subFiles) {
const subPath = path.join(subagentDir, subFile);
if (!fs.existsSync(subPath)) continue;
await readJsonlIncremental(subPath, sessionId, (entry) => {
processEntry(entry, data, toolIdToName);
});
}
}
} catch (err) {
logDebug(LOG_PREFIX, `Extraction error: ${err.message}`);
}
return data;
}
/**
* Compress verbose brain data into compact context summary.
*/
function compressToContext(data) {
const context = {
projectInfo: null,
lastStatus: null,
completed: [],
inProgress: [],
pending: [],
blockers: [],
keyDecisions: [],
compactSummaries: [], // NEW: Separate compact summaries
errors: [],
lastError: null
};
// Extract project info from thinking
if (data.thinking.length > 0) {
const thoughtsToCheck = [
...data.thinking.slice(0, 3),
...data.thinking.slice(-3)
];
const indicators = [
'next.js', 'react', 'vue', 'angular', 'express', 'django', 'flask',
'kindle', 'e-reader', 'dashboard', 'landing', 'api', 'extension',
'news', 'blog', 'portfolio', 'ecommerce', 'app', 'tool', 'script'
];
const foundIndicators = [];
for (const t of thoughtsToCheck) {
const thought = t.thought.toLowerCase();
for (const ind of indicators) {
if (thought.includes(ind) && !foundIndicators.includes(ind)) {
foundIndicators.push(ind);
}
}
}
if (foundIndicators.length > 0) {
context.projectInfo = foundIndicators.slice(0, 3).join(' | ');
}
}
// Parse tasks into categories
if (data.tasks.length > 0) {
const latestTasks = data.tasks[data.tasks.length - 1].todos || [];
for (const todo of latestTasks) {
const status = todo.status || 'pending';
const content = (todo.content || '').trim();
if (!content) continue;
if (status === 'done' || status === 'completed') {
context.completed.push(content);
} else if (status === 'in_progress') {
context.inProgress.push(content);
} else {
context.pending.push(content);
}
}
}
// Collect unique errors
const seenErrors = new Set();
for (const errItem of data.errors) {
const errText = String(errItem.error || '').trim();
const toolName = errItem.tool || '';
// Junk filter
if (errText.length < 5 || errText.startsWith('---') || errText.startsWith('import ')) {
continue;
}
let errKey = null;
// TypeScript/ESLint errors
if (errText.toLowerCase().includes('error:')) {
for (const line of errText.split('\n')) {
if (line.toLowerCase().includes('error') && line.includes(':')) {
errKey = line.trim().substring(0, 500);
break;
}
}
}
// Exit code or command failures (including Windows bash errors)
if (!errKey && (errText.toLowerCase().includes('exit code') ||
errText.toLowerCase().includes('command not found') ||
errText.toLowerCase().includes('bash:') ||
errText.toLowerCase().includes('error: exit code'))) {
const lines = errText.split('\n').filter(l => l.trim());
const junkPatterns = ['starting', 'running', 'inspecting', '...', '---'];
const meaningfulLines = lines.filter(line => {
const lineLower = line.toLowerCase();
const isJunk = junkPatterns.some(jp => lineLower.includes(jp));
const hasError = ['error', 'fail', 'exception', 'invalid', 'not found', 'denied']
.some(ew => lineLower.includes(ew));
return !isJunk || hasError;
});
if (meaningfulLines.length > 0) {
const captured = meaningfulLines.length > 5
? [...meaningfulLines.slice(0, 1), '...', ...meaningfulLines.slice(-4)]
: meaningfulLines;
errKey = captured.join(' | ').substring(0, 600);
} else {
errKey = errText.split('\n').pop()?.substring(0, 500);
}
}
// Build/compile errors
if (!errKey && errText.toLowerCase().includes('failed')) {
errKey = errText.split('\n')[0]?.substring(0, 500);
}
// Deduplicate and add
if (errKey && !seenErrors.has(errKey)) {
seenErrors.add(errKey);
context.errors.push(toolName ? `[${toolName}] ${errKey}` : errKey);
}
}
// Keep last error for blockers
if (context.errors.length > 0) {
context.lastError = context.errors[context.errors.length - 1];
context.blockers.push(context.lastError);
}
// Extract key decisions and separate AUTO-SUMMARY (compact) entries
// We process ALL decisions in the chunk to ensure we don't miss summaries
for (const dec of data.decisions) {
const decisionText = dec.decision || '';
// Check if it's an AUTO-SUMMARY (compact)
if (decisionText.startsWith('AUTO-SUMMARY:')) {
context.compactSummaries.push(decisionText.substring(13).trim()); // Remove "AUTO-SUMMARY: " prefix
} else {
context.keyDecisions.push(decisionText.trim().substring(0, 1000));
}
}
return context;
}
/**
* Write brain.jsonl with consolidated data.
*/
function writeBrainJsonl(sessionId, data, projectRoot) {
try {
const preserved = readPreservedBrain(projectRoot);
const context = compressToContext(data);
const entries = [];
// 1. Tech entries first (preserved)
entries.push(...preserved.tech);
// 2. Compact summaries (preserved + new, keep last 10)
const allCompactsRaw = [...preserved.compacts, ...context.compactSummaries.map(s => ({ type: 'compact', summary: s, ts: getTimestamp() }))];
const uniqueCompacts = [];
const seenCompacts = new Set();
for (const e of allCompactsRaw) {
const txt = (e.summary || e.content || '').trim();
if (txt && !seenCompacts.has(txt)) {
seenCompacts.add(txt);
uniqueCompacts.push(e);
}
}
entries.push(...uniqueCompacts.slice(-10));
// 3. Goals (preserved + new)
const allGoalsRaw = [...preserved.goals];
if (context.projectInfo) {
const exists = allGoalsRaw.some(e => (e.content || '') === context.projectInfo);
if (!exists) {
allGoalsRaw.push({ type: 'goal', content: context.projectInfo, ts: getTimestamp() });
}
}
entries.push(...allGoalsRaw.slice(-20));
// 4. Decisions (merged & deduplicated)
const allDecisionsRaw = [...preserved.decisions];
for (const dec of context.keyDecisions) {
const exists = allDecisionsRaw.some(e => (e.content || e.decision || '') === dec);
if (!exists) {
allDecisionsRaw.push({ type: 'decision', content: dec, session: sessionId });
}
}
entries.push(...allDecisionsRaw.slice(-30));
// 5. Completed items
const allCompletedRaw = [...preserved.completed];
for (const comp of context.completed) {
const exists = allCompletedRaw.some(e => (e.content || '') === comp);
if (!exists) {
allCompletedRaw.push({ type: 'completed', content: comp });
}
}
entries.push(...allCompletedRaw.slice(-30));
// 6. Errors/Blockers
const allErrorsRaw = [...preserved.errors];
for (const err of [...context.errors, ...context.blockers]) {
const exists = allErrorsRaw.some(e => (e.content || e.error || '') === err);
if (!exists) {
allErrorsRaw.push({ type: 'error', content: err });
}
}
entries.push(...allErrorsRaw.slice(-20));
// 8. Others (preserve everything else)
// 8. Others (preserve everything else)
entries.push(...preserved.others);
writeBrain(entries, projectRoot);
logDebug(LOG_PREFIX, `Updated LTM: ${entries.length} entries`);
} catch (err) {
logDebug(LOG_PREFIX, `Write error: ${err.message}`);
}
}
/**
* Extract errors from hook input (stdin).
* Claude Code CLI may send tool information via stdin.
*/
function extractErrorsFromHookInput(hookInput) {
const errors = [];
try {
if (!hookInput || typeof hookInput !== 'object') {
return errors;
}
// Try multiple possible formats
const toolResult = hookInput.toolResult || hookInput.result || hookInput;
const toolName = toolResult.tool_name || toolResult.toolName ||
hookInput.toolName || hookInput.tool || 'unknown';
// Get content from various possible locations
let content = '';
if (toolResult.content) {
content = String(toolResult.content);
} else if (toolResult.output) {
content = String(toolResult.output);
} else if (toolResult.stderr) {
content = String(toolResult.stderr);
} else if (hookInput.content) {
content = String(hookInput.content);
} else if (typeof toolResult === 'string') {
content = toolResult;
}
content = cleanAnsi(content).trim();
// Check if it's an error
const isError = toolResult.is_error || toolResult.isError ||
hookInput.isError || false;
const contentLower = content.toLowerCase();
const errorKeywords = [
'exit code 127', 'exit code 1', 'exit code 2', 'exit code',
'command not found', 'bash: command not found',
'is not recognized as an internal or external command',
'is not recognized as the name of a cmdlet',
'failed to compile', 'build failed', 'compilation failed',
'error:', 'typeerror:', 'syntaxerror:',
'referenceerror:', 'cannot find module', 'module not found',
'command failed', 'npm err!', 'npm error',
'fatal:', 'exception:', 'traceback',
'no such file or directory', 'permission denied',
'access is denied', 'cannot find the path specified',
'error: exit code', 'error exit code'
];
const hasError = isError || errorKeywords.some(kw => contentLower.includes(kw));
if (hasError && content.length > 5) {
errors.push({
timestamp: getTimestamp(),
error: content.substring(0, 1000),
tool: toolName
});
logDebug(LOG_PREFIX, `Captured error from hook input: ${toolName} - ${content.substring(0, 100)}`);
}
} catch (err) {
logDebug(LOG_PREFIX, `Error extracting from hook input: ${err.message}`);
}
return errors;
}
/**
* Write errors directly to brain.jsonl (fallback when session not found).
*/
function writeErrorsToBrain(errors, projectRoot) {
if (errors.length === 0) {
return;
}
try {
const preserved = readPreservedBrain(projectRoot);
const entries = [];
// Preserve tech entries
for (const techEntry of preserved.techEntries) {
entries.push(techEntry);
}
// Preserve existing errors
const existingErrors = preserved.errors || [];
const allErrors = [...existingErrors];
// Add new errors
for (const err of errors) {
const errText = err.error || '';
const toolName = err.tool || '';
const errorEntry = toolName ? `[${toolName}] ${errText}` : errText;
if (errText.length > 10 && !allErrors.includes(errorEntry)) {
allErrors.push(errorEntry);
}
}
// Keep last 20 errors
for (const err of allErrors.slice(-20)) {
entries.push({
type: 'error',
content: err,
ts: getTimestamp()
});
}
const { writeBrain } = require('./lib/brain');
writeBrain(entries, projectRoot);
logDebug(LOG_PREFIX, `Wrote ${errors.length} errors to brain.jsonl (fallback)`);
} catch (err) {
logDebug(LOG_PREFIX, `Error writing errors: ${err.message}`);
}
}
/**
* Main hook entry point.
*/
async function main() {
const projectRoot = findProjectRoot();
try {
const hookInput = await readStdin();
logDebug(LOG_PREFIX, `Hook input keys: ${Object.keys(hookInput || {}).join(', ')}`);
const eventName = hookInput.hook_event_name || hookInput.hookEventName || '';
const toolName = hookInput.toolName || (hookInput.toolResult && hookInput.toolResult.tool_name) || '';
logDebug(LOG_PREFIX, `Event: ${eventName}, Tool: ${toolName}`);
// SHORT-CIRCUIT: Skip sync for read-only tools to avoid heavy processing
if (toolName && isReadOnlyTool(toolName)) {
logDebug(LOG_PREFIX, `Short-circuit: Skipping sync for read-only tool: ${toolName}`);
outputJson({});
return;
}
// Extract errors from hook input first (immediate capture)
const immediateErrors = extractErrorsFromHookInput(hookInput);
if (immediateErrors.length > 0) {
logDebug(LOG_PREFIX, `Found ${immediateErrors.length} immediate errors`);
writeErrorsToBrain(immediateErrors, projectRoot);
}
// Detect session
const { sessionId, mainJsonl, subagentDir } = getActiveSession(projectRoot);
if (!sessionId) {
// Even without session, we can write errors
if (immediateErrors.length > 0) {
logDebug(LOG_PREFIX, 'Session not found, but errors written');
}
outputJson({});
return;
}
// Throttling:
// If no immediate errors and the last sync was very recent, consider skipping
// to avoid excessive JSONL parsing on every tool use.
// BYPASS throttling for critical events to ensure memory continuity.
const priorityEvents = ['UserPromptSubmit', 'Stop', 'SubagentStop', 'PreCompact', 'SessionStart'];
const state = loadState('brain-sync', projectRoot) || {};
const now = Date.now();
const isPriorityEvent = priorityEvents.includes(eventName) || immediateErrors.length > 0;
if (!isPriorityEvent && state.lastSync && (now - state.lastSync < 30000)) {
logDebug(LOG_PREFIX, `Throttling sync (last one was < 30s ago). Event: ${eventName}`);
outputJson({});
return;
}
// Extract data from JSONL files
// If this is a compaction/stop event, we might need to wait for Claude to flush the transcript
let data;
if (eventName === 'Stop' || eventName === 'PreCompact') {
logDebug(LOG_PREFIX, 'Stop/PreCompact event: using retry logic for transcript flush');
for (let attempt = 1; attempt <= 3; attempt++) {
data = await extractBrainData(sessionId, mainJsonl, subagentDir);
// If we found a compact summary that isn't already the last one in our brain, we're good
if (data.compact && data.compact.length > 0) break;
if (attempt < 3) await new Promise(r => setTimeout(r, 1000));
}
} else {
data = await extractBrainData(sessionId, mainJsonl, subagentDir);
}
// Merge immediate errors
if (immediateErrors.length > 0) {
data.errors = [...immediateErrors, ...(data.errors || [])];
}
// Write brain JSONL
writeBrainJsonl(sessionId, data, projectRoot);
// Save sync state
saveState('brain-sync', { lastSync: now, lastSessionId: sessionId }, projectRoot);
outputJson({});
} catch (err) {
logDebug(LOG_PREFIX, `Hook error: ${err.message}`);
logDebug(LOG_PREFIX, `Stack: ${err.stack}`);
outputJson({});
}
}
main();
{
"description": "Maestro plugin hooks for AI memory (LTM) and Ralph Wiggum persistence",
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.js\""
}
]
}
],
"PreCompact": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/pre-compact.js\""
},
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/brain-sync.js\""
}
]
}
],
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "echo {}"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/stop.js\""
},
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/brain-sync.js\""
}
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/brain-sync.js\""
}
]
}
],
"PostToolUseFailure": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/brain-sync.js\""
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/brain-sync.js\""
}
]
}
]
}
}/**
* Brain (Long-Term Memory) Operations
* Manages brain.jsonl for persistent project context across sessions
*
* @module hooks/lib/brain
*/
const fs = require('fs');
const path = require('path');
const { getMaestroDir, ensureMaestroDir, getTimestamp, logDebug } = require('./utils');
const LOG_PREFIX = '[BRAIN]';
/**
* Get brain.jsonl file path.
*
* @param {string} [projectRoot] - Project root path
* @returns {string} brain.jsonl path
*/
function getBrainPath(projectRoot = null) {
return path.join(getMaestroDir(projectRoot), 'brain.jsonl');
}
/**
* Read all entries from brain.jsonl.
*
* @param {string} [projectRoot] - Project root path
* @returns {Array<object>} Array of brain entries
*/
function readBrain(projectRoot = null) {
const brainPath = getBrainPath(projectRoot);
const entries = [];
if (!fs.existsSync(brainPath)) {
return entries;
}
try {
const content = fs.readFileSync(brainPath, 'utf-8');
const lines = content.split('\n').filter(line => line.trim());
for (const line of lines) {
try {
entries.push(JSON.parse(line));
} catch (err) {
logDebug(LOG_PREFIX, `Failed to parse line: ${line.substring(0, 50)}...`);
}
}
} catch (err) {
logDebug(LOG_PREFIX, `Error reading brain: ${err.message}`);
}
return entries;
}
/**
* Write entries to brain.jsonl (overwrites existing).
*
* @param {Array<object>} entries - Entries to write
* @param {string} [projectRoot] - Project root path
*/
function writeBrain(entries, projectRoot = null) {
const maestroDir = ensureMaestroDir(projectRoot);
const brainPath = path.join(maestroDir, 'brain.jsonl');
try {
const content = entries.map(e => JSON.stringify(e)).join('\n') + '\n';
fs.writeFileSync(brainPath, content, 'utf-8');
logDebug(LOG_PREFIX, `Wrote ${entries.length} entries to brain.jsonl`);
} catch (err) {
logDebug(LOG_PREFIX, `Error writing brain: ${err.message}`);
}
}
/**
* Append a single entry to brain.jsonl.
*
* @param {object} entry - Entry to append
* @param {string} [projectRoot] - Project root path
*/
function appendToBrain(entry, projectRoot = null) {
const maestroDir = ensureMaestroDir(projectRoot);
const brainPath = path.join(maestroDir, 'brain.jsonl');
try {
fs.appendFileSync(brainPath, JSON.stringify(entry) + '\n', 'utf-8');
logDebug(LOG_PREFIX, `Appended entry type=${entry.type} to brain.jsonl`);
} catch (err) {
logDebug(LOG_PREFIX, `Error appending to brain: ${err.message}`);
}
}
/**
* Read brain entries by type.
*
* @param {string} type - Entry type to filter
* @param {string} [projectRoot] - Project root path
* @returns {Array<object>} Filtered entries
*/
function readBrainByType(type, projectRoot = null) {
const entries = readBrain(projectRoot);
return entries.filter(e => e.type === type);
}
/**
* Read preserved data from brain.jsonl.
* Preserves tech_stack, architecture, scripts, and compact entries.
*
* @param {string} [projectRoot] - Project root path
* @returns {object} Preserved data categorized
*/
function readPreservedBrain(projectRoot = null) {
const preserved = {
errors: [],
decisions: [],
completed: [],
goals: [],
tech: [],
compacts: [],
others: [] // Catch-all to prevent data loss
};
const entries = readBrain(projectRoot);
for (const entry of entries) {
const type = entry.type;
if (['tech_stack', 'architecture', 'scripts'].includes(type)) {
preserved.tech.push(entry);
} else if (type === 'compact') {
preserved.compacts.push(entry);
} else if (type === 'error') {
preserved.errors.push(entry);
} else if (type === 'decision') {
preserved.decisions.push(entry);
} else if (type === 'completed') {
preserved.completed.push(entry);
} else if (type === 'goal') {
preserved.goals.push(entry);
} else {
preserved.others.push(entry);
}
}
return preserved;
}
/**
* Format brain.jsonl for AI context display.
*
* @param {string} [projectRoot] - Project root path
* @returns {string|null} Formatted brain summary
*/
function formatBrainForContext(projectRoot = null) {
const entries = readBrain(projectRoot);
if (entries.length === 0) {
return null;
}
const categories = {
goal: [],
decision: [],
completed: [],
error: [],
tech_stack: null,
architecture: null,
scripts: null,
compact: []
};
for (const entry of entries) {
const type = entry.type;
if (type === 'tech_stack') {
categories.tech_stack = entry;
} else if (type === 'architecture') {
categories.architecture = entry;
} else if (type === 'scripts') {
categories.scripts = entry;
} else if (type === 'compact') {
const ts = entry.ts || '';
const summary = entry.summary || '';
if (summary) {
categories.compact.push(`[${ts}] ${summary}`);
}
} else if (categories[type] !== undefined) {
const content = entry.content || entry.decision || entry.error;
if (content) {
categories[type].push(content);
}
}
}
const output = [];
// Tech Stack Section
const tech = categories.tech_stack;
const arch = categories.architecture;
const scripts = categories.scripts;
if (tech) {
output.push('### 🔧 Tech Stack');
if (tech.project_name) output.push(`**Project:** ${tech.project_name}`);
if (tech.frameworks && tech.frameworks.length) {
output.push(`**Frameworks:** ${tech.frameworks.join(', ')}`);
}
if (tech.key_deps && tech.key_deps.length) {
output.push(`**Key Dependencies:** ${tech.key_deps.slice(0, 8).join(', ')}`);
}
if (tech.dev_tools && tech.dev_tools.length) {
output.push(`**Dev Tools:** ${tech.dev_tools.join(', ')}`);
}
if (tech.package_manager) {
output.push(`**Package Manager:** ${tech.package_manager}`);
}
}
if (arch) {
output.push('\n### 🏗️ Architecture');
if (arch.patterns && arch.patterns.length) {
output.push(`**Patterns:** ${arch.patterns.join(', ')}`);
}
if (arch.key_directories && arch.key_directories.length) {
output.push(`**Key Dirs:** ${arch.key_directories.slice(0, 6).join(', ')}`);
}
if (arch.entry_points && arch.entry_points.length) {
output.push(`**Entry Points:** ${arch.entry_points.slice(0, 3).join(', ')}`);
}
}
if (scripts && scripts.available) {
output.push('\n### 📜 Available Scripts');
const scriptList = Object.entries(scripts.available).slice(0, 5);
for (const [name, cmd] of scriptList) {
const cmdShort = cmd.length > 50 ? cmd.substring(0, 50) + '...' : cmd;
output.push(`- \`${name}\`: ${cmdShort}`);
}
}
// Compact History
if (categories.compact.length > 0) {
output.push('\n### 📦 Recent project history (Compacted)');
// STRICTLY show only the single LATEST one to avoid verbosity
const latest = categories.compact[categories.compact.length - 1];
output.push(`👉 **LATEST:** ${latest}`);
}
// Standard Brain Sections
if (categories.goal.length > 0) {
output.push('\n### 🎯 Project Goals');
for (const g of categories.goal.slice(-3)) {
output.push(`- ${g}`);
}
}
if (categories.decision.length > 0) {
output.push('\n### 🧠 Key Decisions');
for (const d of categories.decision.slice(-3)) { // Reduced from 5 to 3
// Truncate long decisions
const decisionText = d.length > 200 ? d.substring(0, 200) + '...' : d;
output.push(`- ${decisionText}`);
}
}
if (categories.completed.length > 0) {
output.push('\n### ✅ Completed');
for (const c of categories.completed.slice(-3)) { // Reduced from 5 to 3
// Truncate long completed items
const completedText = c.length > 150 ? c.substring(0, 150) + '...' : c;
output.push(`- ${completedText}`);
}
}
if (categories.error.length > 0) {
output.push('\n### 🚓 Known Issues/Errors');
for (const e of categories.error.slice(-2)) { // Reduced from 3 to 2
// Truncate long errors
const errorText = e.length > 300 ? e.substring(0, 300) + '...' : e;
output.push(`- ${errorText}`);
}
}
return output.length > 0 ? output.join('\n') : 'No established long-term memory yet.';
}
/**
* Write tech stack info to brain.jsonl.
*
* @param {object} techInfo - Tech stack information
* @param {object} structure - Project structure information
* @param {string} [projectRoot] - Project root path
*/
function writeTechToBrain(techInfo, structure, projectRoot = null) {
const preserved = readPreservedBrain(projectRoot);
const ts = getTimestamp();
// Create new tech entries
const techEntry = {
type: 'tech_stack',
ts,
project_name: techInfo.name,
project_version: techInfo.version,
description: techInfo.description,
frameworks: techInfo.frameworks || [],
framework_versions: techInfo.frameworkVersions || {},
key_deps: techInfo.keyDeps || [],
dev_tools: techInfo.devTools || [],
package_manager: techInfo.packageManager,
node_version: techInfo.nodeVersion,
module_type: techInfo.type
};
const archEntry = {
type: 'architecture',
ts,
project_type: structure.type,
patterns: structure.patterns || [],
key_directories: structure.keyDirectories || [],
entry_points: structure.entryPoints || []
};
const scriptsEntry = {
type: 'scripts',
ts,
available: techInfo.scripts || {}
};
// Reconstruct entries: New tech entries first, then everything else
const newEntries = [
techEntry,
archEntry,
scriptsEntry,
...preserved.compacts,
...preserved.goals,
...preserved.decisions,
...preserved.completed,
...preserved.errors,
...preserved.others
];
writeBrain(newEntries, projectRoot);
logDebug(LOG_PREFIX, 'Tech stack written to brain.jsonl');
}
/**
* Deduplicate an array while preserving order.
*
* @param {Array} arr - Array to deduplicate
* @returns {Array} Deduplicated array
*/
function dedupe(arr) {
return [...new Set(arr)];
}
/**
* Extract the last assistant message (likely the compact summary) from a transcript.
*
* @param {string} transcriptPath - Path to JSONL transcript
* @returns {string|null} Extracted summary or null
*/
function extractLastSummary(transcriptPath) {
if (!transcriptPath || !fs.existsSync(transcriptPath)) {
return null;
}
try {
// Safety check for exceptionally large transcripts
const stats = fs.statSync(transcriptPath);
if (stats.size > 100 * 1024 * 1024) { // 100MB safety fuse
logDebug('[BRAIN]', 'Transcript abnormally large (>100MB), skipping extraction');
return null;
}
const content = fs.readFileSync(transcriptPath, 'utf-8');
const lines = content.split('\n').filter(l => l.trim());
let lastAssistantMessage = null;
// Read backwards to find last message that looks like a summary
for (let i = lines.length - 1; i >= 0; i--) {
try {
const entry = JSON.parse(lines[i]);
// DETECTION 1: Explicit flag (The most reliable way in new Claude Code versions)
if (entry.isCompactSummary || entry.is_compact_summary) {
const content = entry.message?.content;
if (typeof content === 'string') {
lastAssistantMessage = content;
} else if (Array.isArray(content)) {
for (const block of content) {
if (block.type === 'text') {
lastAssistantMessage = block.text;
// Don't break here, take the last text block if multiple exist
}
}
}
if (lastAssistantMessage) return lastAssistantMessage; // Found explicit summary, return immediately
}
// DETECTION 2: Pattern matching (fallback)
if (entry.type === 'assistant' || entry.type === 'user') { // Sometimes summaries appear as user messages in compact
const msgContent = entry.message?.content;
if (!msgContent) continue;
// Normalize to array of text blocks
const blocks = typeof msgContent === 'string'
? [{ type: 'text', text: msgContent }]
: (Array.isArray(msgContent) ? msgContent : []);
for (const block of blocks) {
if (block.type === 'text') {
const text = block.text || '';
// Keywords and length check
// We tightened the check to avoid false positives
const isSummary = text.length > 50 && (
(text.includes('This session is being continued from a previous conversation')) ||
(text.includes('The following is a compact summary')) ||
(text.includes('compact') && text.includes('summary') && text.includes('context'))
) && !text.includes('<local-command-stdout>');
if (isSummary) {
lastAssistantMessage = text;
return lastAssistantMessage; // Found highly probable summary, return immediately (since we read backwards)
}
}
}
}
} catch (err) {
// Skip invalid JSON lines
}
}
return lastAssistantMessage;
} catch (err) {
logDebug('[BRAIN]', `Error extracting summary: ${err.message}`);
return null;
}
}
/**
* Write compact summary to brain.jsonl.
*
* @param {string} summary - The summary text
* @param {string} [projectRoot] - Project root path
* @returns {boolean} True if successfully saved
*/
function writeCompactToBrain(summary, projectRoot = null) {
try {
if (!summary || summary.length < 100) {
logDebug('[BRAIN]', 'Summary too short, skipping');
return false;
}
// Read preserved brain data
const preserved = readPreservedBrain(projectRoot);
// Create new compact entry
const compactEntry = {
type: 'compact',
summary: summary, // Limits removed as requested
ts: getTimestamp()
};
// Filter and combine compact entries (keep last 10)
// We deduplicate by content (stripping whitespace) to avoid doubles
const cleanSummary = summary.trim();
const existingCompacts = preserved.compacts
.filter(e => {
const existingText = (e.summary || e.content || '').trim();
return existingText !== cleanSummary;
});
// Take last 9 and add the new one
const finalCompacts = [...existingCompacts.slice(-9), compactEntry];
// PROTECTION: If the most recent compact in preserved is IDENTICAL to the new one,
// and it was added VERY recently (less than 5s ago), it might be a duplicate capture.
if (preserved.compacts.length > 0) {
const last = preserved.compacts[preserved.compacts.length - 1];
const lastText = (last.summary || last.content || '').trim();
if (lastText === cleanSummary) {
logDebug('[BRAIN]', 'Duplicate summary detected, skipping write');
return true; // Consider it handled
}
}
// Reconstruct brain entries (Ordering: Tech -> Compacts -> Goals -> Decisions -> Others)
const entries = [];
// 1. Tech entries
entries.push(...preserved.tech);
// 2. Compact entries
entries.push(...finalCompacts);
// 3. Goals
entries.push(...preserved.goals.slice(-10));
// 4. Decisions
entries.push(...preserved.decisions.slice(-20));
// 5. Completed
entries.push(...preserved.completed.slice(-20));
// 6. Errors
entries.push(...preserved.errors.slice(-10));
// 7. Others (preserve everything else)
entries.push(...preserved.others);
writeBrain(entries, projectRoot);
logDebug('[BRAIN]', `Compact summary saved to brain.jsonl (${summary.length} chars)`);
return true;
} catch (err) {
logDebug('[BRAIN]', `Error writing compact to brain: ${err.message}`);
return false;
}
}
module.exports = {
getBrainPath,
readBrain,
writeBrain,
appendToBrain,
readBrainByType,
readPreservedBrain,
formatBrainForContext,
writeTechToBrain,
extractLastSummary,
writeCompactToBrain,
dedupe
};
/**
* Ralph Wiggum State Manager
*
* Manages Ralph Wiggum iteration state for autonomous QA mode.
* This module handles:
* - Creating and updating .maestro/ralph.state
* - Tracking iteration counts
* - Managing completion signals
*
* @module hooks/lib/ralph
*/
const fs = require('fs');
const path = require('path');
const {
findProjectRoot,
getMaestroDir,
logDebug
} = require('./utils');
const LOG_PREFIX = '[RALPH]';
/**
* Get the Ralph state file path
* @returns {string} Full path to ralph.state
*/
function getStateFilePath() {
const projectRoot = findProjectRoot();
const maestroDir = getMaestroDir(projectRoot);
return path.join(maestroDir, 'ralph.state');
}
/**
* Get the Ralph completion file path
* @returns {string} Full path to ralph.complete
*/
function getCompleteFilePath() {
const projectRoot = findProjectRoot();
const maestroDir = getMaestroDir(projectRoot);
return path.join(maestroDir, 'ralph.complete');
}
/**
* Get the Ralph active sentinel file path
* @returns {string} Full path to ralph.active
*/
function getActiveFilePath() {
const projectRoot = findProjectRoot();
const maestroDir = getMaestroDir(projectRoot);
return path.join(maestroDir, 'ralph.active');
}
/**
* Ralph state structure
* @typedef {Object} RalphState
* @property {number} max - Maximum iterations
* @property {number} current - Current iteration count
* @property {string} mode - Ralph mode (all/debug/feature)
* @property {string[]} features - Active features
* @property {number} startedAt - Timestamp when started
* @property {number} lastUpdate - Timestamp of last update
*/
/**
* Read Ralph state from file
* @returns {RalphState|null} State object or null if not exists
*/
function readState() {
const statePath = getStateFilePath();
if (!fs.existsSync(statePath)) {
return null;
}
try {
const content = fs.readFileSync(statePath, 'utf8');
return JSON.parse(content);
} catch (err) {
logDebug(LOG_PREFIX, `Error reading state: ${err.message}`);
return null;
}
}
/**
* Write Ralph state to file
* @param {RalphState} state - State to write
*/
function writeState(state) {
const statePath = getStateFilePath();
const maestroDir = path.dirname(statePath);
// Ensure .maestro directory exists
if (!fs.existsSync(maestroDir)) {
fs.mkdirSync(maestroDir, { recursive: true });
}
state.lastUpdate = Date.now();
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
logDebug(LOG_PREFIX, `State updated: ${state.current}/${state.max}`);
}
/**
* Initialize Ralph state
* @param {number} maxIterations - Maximum iterations
* @param {string} mode - Ralph mode (all/debug/feature)
* @param {string[]} features - Active features
* @returns {RalphState} Created state
*/
function initializeState(maxIterations, mode = 'all', features = []) {
const state = {
max: maxIterations,
current: 0,
mode: mode,
features: features,
startedAt: Date.now(),
lastUpdate: Date.now()
};
writeState(state);
// Create active sentinel
const activePath = getActiveFilePath();
fs.writeFileSync(activePath, '');
logDebug(LOG_PREFIX, `State initialized: ${maxIterations} iterations, mode=${mode}`);
return state;
}
/**
* Increment iteration counter
* @returns {RalphState|null} Updated state or null if not active
*/
function incrementIteration() {
const state = readState();
if (!state) {
return null;
}
state.current++;
writeState(state);
return state;
}
/**
* Check if Ralph is currently active
* @returns {boolean} True if Ralph is active
*/
function isActive() {
const state = readState();
const activePath = getActiveFilePath();
return state !== null && fs.existsSync(activePath);
}
/**
* Check if Ralph should continue iterating
* @returns {boolean} True if should continue
*/
function shouldContinue() {
const state = readState();
if (!state) {
return false;
}
// Check if completed
const completePath = getCompleteFilePath();
if (fs.existsSync(completePath)) {
logDebug(LOG_PREFIX, 'Completion signal detected');
return false;
}
// Check if max iterations reached
return state.current < state.max;
}
/**
* Check if exit should be blocked
* @returns {Object} Block decision
*/
function getBlockDecision() {
if (!isActive()) {
return { block: false, reason: 'Ralph not active' };
}
const state = readState();
if (!state) {
return { block: false, reason: 'No state found' };
}
// Check completion signal
const completePath = getCompleteFilePath();
if (fs.existsSync(completePath)) {
return { block: false, reason: 'Completion signal detected', completed: true };
}
// Increment iteration
const newState = incrementIteration();
if (!newState) {
return { block: false, reason: 'Failed to increment' };
}
// Check if should continue
if (newState.current <= newState.max) {
return {
block: true,
reason: `Iteration ${newState.current}/${newState.max}`,
current: newState.current,
max: newState.max
};
}
return { block: false, reason: 'Max iterations reached', completed: true };
}
/**
* Mark Ralph as complete
*/
function markComplete() {
const completePath = getCompleteFilePath();
const maestroDir = path.dirname(completePath);
if (!fs.existsSync(maestroDir)) {
fs.mkdirSync(maestroDir, { recursive: true });
}
fs.writeFileSync(completePath, 'completed');
logDebug(LOG_PREFIX, 'Marked as complete');
}
/**
* Clear Ralph state (cleanup)
*/
function clearState() {
const statePath = getStateFilePath();
const activePath = getActiveFilePath();
const completePath = getCompleteFilePath();
if (fs.existsSync(statePath)) {
fs.unlinkSync(statePath);
}
if (fs.existsSync(activePath)) {
fs.unlinkSync(activePath);
}
if (fs.existsSync(completePath)) {
fs.unlinkSync(completePath);
}
logDebug(LOG_PREFIX, 'State cleared');
}
/**
* Get current iteration status
* @returns {Object|null} Status object or null
*/
function getStatus() {
const state = readState();
if (!state) {
return null;
}
const completePath = getCompleteFilePath();
const isComplete = fs.existsSync(completePath);
return {
current: state.current,
max: state.max,
mode: state.mode,
features: state.features,
isComplete: isComplete,
startedAt: state.startedAt,
lastUpdate: state.lastUpdate
};
}
module.exports = {
getStateFilePath,
getCompleteFilePath,
getActiveFilePath,
readState,
writeState,
initializeState,
incrementIteration,
isActive,
shouldContinue,
getBlockDecision,
markComplete,
clearState,
getStatus
};
/**
* Maestro Hook Utilities
* Cross-platform utility functions for all Maestro hooks
*
* @module hooks/lib/utils
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
/**
* Find the project root directory.
*
* Priority order:
* 1. CLAUDE_PROJECT_DIR environment variable (set by Claude Code)
* 2. CLAUDE_WORKING_DIR environment variable (fallback)
* 3. Search upward from current directory for root markers
*
* @param {string} [startDir] - Starting directory for search
* @returns {string} Project root path
*/
function findProjectRoot(startDir = null) {
// Priority 1: Use CLAUDE_PROJECT_DIR if set by Claude Code
const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR;
if (claudeProjectDir && fs.existsSync(claudeProjectDir)) {
logDebug('[UTILS]', `Project root from CLAUDE_PROJECT_DIR: ${claudeProjectDir}`);
return claudeProjectDir;
}
// Priority 2: Use CLAUDE_WORKING_DIR if set
const claudeWorkingDir = process.env.CLAUDE_WORKING_DIR;
if (claudeWorkingDir && fs.existsSync(claudeWorkingDir)) {
logDebug('[UTILS]', `Project root from CLAUDE_WORKING_DIR: ${claudeWorkingDir}`);
return claudeWorkingDir;
}
// Priority 3: Search upward from startDir
let current = path.resolve(startDir || process.cwd());
const rootMarkers = ['.git', 'package.json', '.claude', 'pyproject.toml', 'Cargo.toml'];
// Walk up the directory tree
while (true) {
for (const marker of rootMarkers) {
const markerPath = path.join(current, marker);
if (fs.existsSync(markerPath)) {
return current;
}
}
const parent = path.dirname(current);
if (parent === current) {
// Reached filesystem root
break;
}
current = parent;
}
// Fallback: return original directory
return startDir || process.cwd();
}
/**
* Get the .maestro directory path.
* Always in project root, never in subdirectories.
*
* @param {string} [projectRoot] - Project root path
* @returns {string} .maestro directory path
*/
function getMaestroDir(projectRoot = null) {
const root = projectRoot || findProjectRoot();
return path.join(root, '.maestro');
}
/**
* Ensure .maestro directory exists in project root.
*
* @param {string} [projectRoot] - Project root path
* @returns {string} .maestro directory path
*/
function ensureMaestroDir(projectRoot = null) {
const maestroDir = getMaestroDir(projectRoot);
if (!fs.existsSync(maestroDir)) {
fs.mkdirSync(maestroDir, { recursive: true });
}
return maestroDir;
}
/**
* Get the plugin root directory.
*
* @returns {string} Plugin root path
*/
function getPluginRoot() {
return process.env.CLAUDE_PLUGIN_ROOT || path.resolve(__dirname, '..', '..');
}
/**
* Get the Claude projects directory (cross-platform).
*
* @returns {string} Claude projects directory path
*/
function getClaudeProjectsDir() {
if (process.platform === 'win32') {
// Windows: Claude Code uses USERPROFILE\.claude\projects (not APPDATA)
// Check both locations for compatibility
const userProfile = process.env.USERPROFILE || os.homedir();
const userProfilePath = path.join(userProfile, '.claude', 'projects');
if (fs.existsSync(userProfilePath)) {
return userProfilePath;
}
// Fallback to APPDATA for older installations
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
return path.join(appData, '.claude', 'projects');
}
// macOS/Linux: Use home directory
return path.join(os.homedir(), '.claude', 'projects');
}
/**
* Normalize a project path for Claude's folder naming convention.
* Claude Code normalizes paths: C:\Users\foo -> C--Users-foo
*
* @param {string} rawPath - Raw file system path
* @returns {string} Normalized path string
*/
function normalizeProjectPath(rawPath) {
if (process.platform === 'win32' && rawPath.includes(':')) {
let [drive, rest] = rawPath.split(':');
// Force drive letter to uppercase for consistent matching
drive = drive.toUpperCase();
// Remove leading slashes and replace all slashes/dots with dashes
const normalized = rest.replace(/^[\\/]+/, '').replace(/[\\/]/g, '-').replace(/\./g, '-');
return `${drive}--${normalized}`;
}
return rawPath.replace(/[\\/]/g, '-').replace(/\./g, '-');
}
/**
* Log message to stderr (for debugging).
* Only logs when MAESTRO_DEBUG=1
*
* @param {string} prefix - Log prefix (e.g., '[BRAIN]')
* @param {string} msg - Message to log
*/
function logDebug(prefix, msg) {
if (process.env.MAESTRO_DEBUG === '1') {
process.stderr.write(`${prefix} ${msg}\n`);
}
}
/**
* Read a file safely with size limit.
*
* @param {string} filePath - Path to file
* @param {number} [maxSize=100000] - Maximum file size in bytes
* @returns {string|null} File contents or null if failed/too large
*/
function readFileSafe(filePath, maxSize = 100000) {
try {
if (!fs.existsSync(filePath)) {
return null;
}
const stats = fs.statSync(filePath);
if (stats.size > maxSize) {
logDebug('[UTILS]', `File too large, skipping: ${filePath}`);
return null;
}
return fs.readFileSync(filePath, 'utf-8');
} catch (err) {
logDebug('[UTILS]', `Error reading ${filePath}: ${err.message}`);
return null;
}
}
/**
* Remove ANSI escape codes from text.
*
* @param {string} text - Text with potential ANSI codes
* @returns {string} Clean text
*/
function cleanAnsi(text) {
// ANSI escape code pattern
return text.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
}
/**
* Get current timestamp in ISO format.
*
* @returns {string} Timestamp string
*/
function getTimestamp() {
return new Date().toISOString().replace('T', ' ').substring(0, 19);
}
/**
* Get current time in HH:MM:SS format.
*
* @returns {string} Time string
*/
function getTimeOnly() {
return new Date().toISOString().substring(11, 19);
}
/**
* Read JSON from stdin (for hook input).
*
* @returns {Promise<object>} Parsed JSON object
*/
function readStdin() {
return new Promise((resolve, reject) => {
let data = '';
process.stdin.setEncoding('utf-8');
process.stdin.on('data', (chunk) => {
data += chunk;
});
process.stdin.on('end', () => {
if (!data.trim()) {
resolve({});
return;
}
try {
resolve(JSON.parse(data));
} catch (err) {
logDebug('[UTILS]', `JSON parse error: ${err.message}`);
resolve({});
}
});
process.stdin.on('error', (err) => {
logDebug('[UTILS]', `stdin error: ${err.message}`);
resolve({});
});
// Handle case where stdin is already closed or empty
if (process.stdin.readableEnded) {
resolve({});
}
});
}
/**
* Output JSON to stdout (for hook output).
*
* @param {object} data - Data to output
*/
function outputJson(data) {
console.log(JSON.stringify(data));
}
/**
* Smart truncation - keeps beginning and end for context.
*
* @param {string} text - Text to truncate
* @param {number} maxLen - Maximum length
* @returns {string} Truncated text
*/
function truncateSmart(text, maxLen) {
if (text.length <= maxLen) {
return text;
}
// Keep 60% from start, 40% from end
const startLen = Math.floor(maxLen * 0.6);
const endLen = maxLen - startLen - 20; // Reserve space for separator
return text.substring(0, startLen) + '\n...[truncated]...\n' + text.substring(text.length - endLen);
}
/**
* Calculate MD5 hash of a string (simple implementation for change detection).
*
* @param {string} content - Content to hash
* @returns {string} Hash string
*/
function simpleHash(content) {
const crypto = require('crypto');
return crypto.createHash('md5').update(content).digest('hex');
}
/**
* Get file hash for change detection.
*
* @param {string} filePath - Path to file
* @returns {string|null} File hash or null if failed
*/
function getFileHash(filePath) {
try {
if (!fs.existsSync(filePath)) {
return null;
}
const content = fs.readFileSync(filePath);
return simpleHash(content.toString());
} catch (err) {
return null;
}
}
const { execSync } = require('child_process');
/**
* Check if the current project is a Git repository.
*
* @param {string} projectRoot - Project root path
* @returns {boolean} True if Git repo
*/
function isGitProject(projectRoot) {
try {
return fs.existsSync(path.join(projectRoot, '.git'));
} catch (err) {
return false;
}
}
/**
* Get changed files using Git (much faster than recursive scan).
*
* @param {string} projectRoot - Project root path
* @returns {string[]} Array of relative file paths
*/
function getGitDirtyFiles(projectRoot) {
try {
// --porcelain=v1 gives a predictable, machine-readable output
const output = execSync('git status --porcelain=v1', { cwd: projectRoot, encoding: 'utf-8' });
return output.split('\n')
.map(line => line.substring(3).trim())
.filter(line => line.length > 0);
} catch (err) {
logDebug('[UTILS]', `Git status failed: ${err.message}`);
return [];
}
}
/**
* Save state to .maestro directory.
*
* @param {string} name - State file name (without extension)
* @param {object} data - Data to save
* @param {string} [projectRoot] - Project root path
*/
function saveState(name, data, projectRoot = null) {
try {
const maestroDir = ensureMaestroDir(projectRoot);
const stateFile = path.join(maestroDir, `${name}.state`);
fs.writeFileSync(stateFile, JSON.stringify(data, null, 2), 'utf-8');
} catch (err) {
logDebug('[UTILS]', `Error saving state ${name}: ${err.message}`);
}
}
/**
* Load state from .maestro directory.
*
* @param {string} name - State file name
* @param {string} [projectRoot] - Project root path
* @returns {object|null} State data or null
*/
function loadState(name, projectRoot = null) {
try {
const maestroDir = getMaestroDir(projectRoot);
const stateFile = path.join(maestroDir, `${name}.state`);
if (fs.existsSync(stateFile)) {
return JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
}
} catch (err) {
logDebug('[UTILS]', `Error loading state ${name}: ${err.message}`);
}
return null;
}
/**
* Check if a tool is read-only (unlikely to change project state).
*
* @param {string} toolName - Name of the tool
* @returns {boolean} True if read-only
*/
function isReadOnlyTool(toolName) {
const readOnlyTools = [
'view_file', 'Read', 'read_file',
'list_dir', 'LS', 'ls', 'dir',
'grep_search', 'Grep', 'search',
'find_by_name', 'Glob', 'glob',
'read_url_content', 'read_browser_page',
'list_resources', 'read_resource',
'command_status', 'read_terminal',
'AskUserQuestion', 'ask'
];
return readOnlyTools.includes(toolName);
}
module.exports = {
findProjectRoot,
getMaestroDir,
ensureMaestroDir,
getPluginRoot,
getClaudeProjectsDir,
isGitProject,
getGitDirtyFiles,
saveState,
loadState,
isReadOnlyTool,
normalizeProjectPath,
logDebug,
readFileSafe,
cleanAnsi,
getTimestamp,
getTimeOnly,
readStdin,
outputJson,
truncateSmart,
simpleHash,
getFileHash
};
#!/usr/bin/env node
/**
* Pre-Compact Hook - Compact Summary Capture
* Captures compact summaries before/after manual /compact command
* and persists them to brain.jsonl for cross-session memory.
*
* @event PreCompact
* @matcher manual - Triggered by /compact command
*/
const fs = require('fs');
const path = require('path');
const {
findProjectRoot,
getMaestroDir,
ensureMaestroDir,
getClaudeProjectsDir,
normalizeProjectPath,
loadState,
saveState,
logDebug,
readStdin,
outputJson
} = require('./lib/utils');
const {
readBrain,
writeBrain,
appendToBrain,
extractLastSummary,
writeCompactToBrain
} = require('./lib/brain');
const LOG_PREFIX = '[PRE-COMPACT]';
/**
* Find the current active session transcript.
*/
function getCurrentTranscript() {
try {
const claudeProjectsDir = getClaudeProjectsDir();
if (!fs.existsSync(claudeProjectsDir)) {
return null;
}
const projectRoot = findProjectRoot();
const cwdNormalized = normalizeProjectPath(projectRoot);
let projectDir = null;
// Find matching project directory (case-insensitive for Windows compatibility)
const entries = fs.readdirSync(claudeProjectsDir);
for (const entry of entries) {
if (entry.toLowerCase() === cwdNormalized.toLowerCase()) {
projectDir = path.join(claudeProjectsDir, entry);
break;
}
}
// Prefix match fallback (case-insensitive)
if (!projectDir) {
for (const entry of entries) {
const entryLower = entry.toLowerCase();
const cwdLower = cwdNormalized.toLowerCase();
if (cwdLower && (entryLower.startsWith(cwdLower) || cwdLower.startsWith(entryLower))) {
projectDir = path.join(claudeProjectsDir, entry);
break;
}
}
}
if (!projectDir) {
return null;
}
// Find most recent JSONL file
const jsonlFiles = fs.readdirSync(projectDir)
.filter(f => f.endsWith('.jsonl'))
.map(f => ({
name: f,
path: path.join(projectDir, f),
mtime: fs.statSync(path.join(projectDir, f)).mtimeMs
}))
.sort((a, b) => b.mtime - a.mtime);
if (jsonlFiles.length > 0) {
return jsonlFiles[0].path;
}
return null;
} catch (err) {
logDebug(LOG_PREFIX, `Error finding transcript: ${err.message}`);
return null;
}
}
/**
* Main hook entry point.
*/
async function main() {
const projectRoot = findProjectRoot();
logDebug(LOG_PREFIX, '='.repeat(60));
logDebug(LOG_PREFIX, 'PRE-COMPACT HOOK TRIGGERED');
logDebug(LOG_PREFIX, `Project Root: ${projectRoot}`);
try {
// Read hook input
const hookInput = await readStdin();
logDebug(LOG_PREFIX, `Hook input: trigger=${hookInput.trigger}, custom_instructions=${hookInput.custom_instructions ? 'yes' : 'no'}`);
// Only process manual compacts
if (hookInput.trigger !== 'manual') {
outputJson({});
return;
}
// Get current transcript
const transcriptPath = hookInput.transcript_path || getCurrentTranscript();
logDebug(LOG_PREFIX, `Current transcript: ${transcriptPath ? path.basename(transcriptPath) : 'none'}`);
if (!transcriptPath) {
outputJson({});
return;
}
// Extract last summary from transcript
const summary = extractLastSummary(transcriptPath);
logDebug(LOG_PREFIX, `Summary found: ${summary ? 'yes (' + summary.length + ' chars)' : 'no'}`);
if (summary) {
// Write to brain.jsonl
writeCompactToBrain(summary, projectRoot);
// Also save to state for reference if needed
saveState('last-compact', {
summary: summary.substring(0, 5000),
timestamp: Date.now(),
trigger: 'manual'
}, projectRoot);
// Output context for the new session
outputJson({
type: 'compact_capture',
hookSpecificOutput: {
hookEventName: 'PreCompact',
additionalContext: `
✅ Current summary captured and saved to brain.jsonl.
Note: Compaction will now continue and a new summary will be generated.
`
}
});
logDebug(LOG_PREFIX, 'Pre-compact hook completed successfully');
} else {
// It's normal to not find a summary in PreCompact because it hasn't been generated yet for this compaction.
// But we output OK anyway.
outputJson({});
logDebug(LOG_PREFIX, 'No pre-existing compact summary found in current transcript.');
}
} catch (err) {
logDebug(LOG_PREFIX, `Hook error: ${err.message}`);
logDebug(LOG_PREFIX, `Stack: ${err.stack}`);
outputJson({});
}
}
main();
#!/usr/bin/env node
/**
* Session Start Hook for Maestro Plugin
* Reads brain.jsonl and project context when a new session begins.
* Analyzes package.json for tech stack info and stores in LTM.
*
* @event SessionStart
*/
const fs = require('fs');
const path = require('path');
const {
findProjectRoot,
getMaestroDir,
ensureMaestroDir,
readFileSafe,
getFileHash,
readStdin,
logDebug,
outputJson
} = require('./lib/utils');
const {
formatBrainForContext,
writeTechToBrain,
extractLastSummary,
writeCompactToBrain
} = require('./lib/brain');
const LOG_PREFIX = '[SESSION-START]';
// Framework detection patterns
const FRAMEWORK_PATTERNS = {
'next.js': { deps: ['next'], files: ['next.config.js', 'next.config.mjs', 'next.config.ts'] },
'react': { deps: ['react', 'react-dom'], files: [] },
'vue': { deps: ['vue'], files: ['vue.config.js', 'nuxt.config.js'] },
'nuxt': { deps: ['nuxt'], files: ['nuxt.config.js', 'nuxt.config.ts'] },
'angular': { deps: ['@angular/core'], files: ['angular.json'] },
'svelte': { deps: ['svelte'], files: ['svelte.config.js'] },
'express': { deps: ['express'], files: [] },
'fastify': { deps: ['fastify'], files: [] },
'nestjs': { deps: ['@nestjs/core'], files: ['nest-cli.json'] },
'electron': { deps: ['electron'], files: [] },
'tauri': { deps: ['@tauri-apps/api'], files: ['tauri.conf.json'] },
'astro': { deps: ['astro'], files: ['astro.config.mjs'] },
'remix': { deps: ['@remix-run/react'], files: ['remix.config.js'] },
'gatsby': { deps: ['gatsby'], files: ['gatsby-config.js'] }
};
// Important dependencies to track
const IMPORTANT_DEPS = {
// State management
'zustand': 'State (Zustand)',
'redux': 'State (Redux)',
'recoil': 'State (Recoil)',
'jotai': 'State (Jotai)',
'mobx': 'State (MobX)',
'@tanstack/react-query': 'Data Fetching (React Query)',
'swr': 'Data Fetching (SWR)',
// Styling
'tailwindcss': 'Styling (Tailwind)',
'styled-components': 'Styling (Styled Components)',
'@emotion/react': 'Styling (Emotion)',
'sass': 'Styling (SASS)',
'@mui/material': 'UI (Material UI)',
'@chakra-ui/react': 'UI (Chakra)',
'antd': 'UI (Ant Design)',
'shadcn-ui': 'UI (shadcn)',
// Database
'prisma': 'ORM (Prisma)',
'@prisma/client': 'ORM (Prisma)',
'drizzle-orm': 'ORM (Drizzle)',
'typeorm': 'ORM (TypeORM)',
'mongoose': 'ODM (Mongoose)',
'sequelize': 'ORM (Sequelize)',
// Auth
'next-auth': 'Auth (NextAuth)',
'@clerk/nextjs': 'Auth (Clerk)',
'@supabase/supabase-js': 'Backend (Supabase)',
'firebase': 'Backend (Firebase)',
// Testing
'jest': 'Testing (Jest)',
'vitest': 'Testing (Vitest)',
'@testing-library/react': 'Testing (RTL)',
'playwright': 'E2E (Playwright)',
'cypress': 'E2E (Cypress)',
// Build tools
'vite': 'Build (Vite)',
'webpack': 'Build (Webpack)',
'esbuild': 'Build (esbuild)',
'turbo': 'Monorepo (Turborepo)',
// Utilities
'zod': 'Validation (Zod)',
'yup': 'Validation (Yup)',
'axios': 'HTTP (Axios)',
'date-fns': 'Dates (date-fns)',
'dayjs': 'Dates (Day.js)',
'lodash': 'Utils (Lodash)',
'framer-motion': 'Animation (Framer)'
};
/**
* Check if tech stack needs re-analysis (package.json changed).
*/
function shouldReanalyzeTech(projectRoot) {
const pkgPath = path.join(projectRoot, 'package.json');
const hashFile = path.join(getMaestroDir(projectRoot), '.tech_hash');
const currentHash = getFileHash(pkgPath);
if (!currentHash) {
return false; // No package.json
}
if (fs.existsSync(hashFile)) {
try {
const storedHash = fs.readFileSync(hashFile, 'utf-8').trim();
if (storedHash === currentHash) {
return false; // No change
}
} catch (err) {
// Continue with reanalysis
}
}
return true;
}
/**
* Save current package.json hash.
*/
function saveTechHash(projectRoot) {
const pkgPath = path.join(projectRoot, 'package.json');
const hashFile = path.join(ensureMaestroDir(projectRoot), '.tech_hash');
const hash = getFileHash(pkgPath);
if (hash) {
try {
fs.writeFileSync(hashFile, hash, 'utf-8');
} catch (err) {
logDebug(LOG_PREFIX, `Error saving tech hash: ${err.message}`);
}
}
}
/**
* Analyze package.json and extract tech stack info.
*/
function analyzePackageJson(projectRoot) {
const pkgPath = path.join(projectRoot, 'package.json');
if (!fs.existsSync(pkgPath)) {
logDebug(LOG_PREFIX, 'No package.json found');
return null;
}
try {
const pkgContent = fs.readFileSync(pkgPath, 'utf-8');
const pkg = JSON.parse(pkgContent);
const result = {
name: pkg.name || 'unknown',
version: pkg.version || '0.0.0',
description: pkg.description || '',
frameworks: [],
frameworkVersions: {},
keyDeps: [],
devTools: [],
scripts: {},
nodeVersion: null,
packageManager: null,
type: pkg.type || 'commonjs'
};
// Combine all dependencies
const allDeps = {
...pkg.dependencies,
...pkg.devDependencies
};
// Detect frameworks
for (const [framework, patterns] of Object.entries(FRAMEWORK_PATTERNS)) {
// Check dependencies
for (const dep of patterns.deps) {
if (allDeps[dep]) {
if (!result.frameworks.includes(framework)) {
result.frameworks.push(framework);
}
// Capture version
const version = allDeps[dep].replace(/[\^~>=<]/g, '');
result.frameworkVersions[dep] = version;
break;
}
}
// Check config files
for (const cfgFile of patterns.files) {
if (fs.existsSync(path.join(projectRoot, cfgFile))) {
if (!result.frameworks.includes(framework)) {
result.frameworks.push(framework);
}
break;
}
}
}
// Always capture React version if present
if (allDeps['react'] && !result.frameworkVersions['react']) {
result.frameworkVersions['react'] = allDeps['react'].replace(/[\^~>=<]/g, '');
}
// Capture TypeScript version
if (allDeps['typescript']) {
result.frameworkVersions['typescript'] = allDeps['typescript'].replace(/[\^~>=<]/g, '');
}
// Capture Tailwind version
if (allDeps['tailwindcss']) {
result.frameworkVersions['tailwindcss'] = allDeps['tailwindcss'].replace(/[\^~>=<]/g, '');
}
// Detect important dependencies
for (const [dep, label] of Object.entries(IMPORTANT_DEPS)) {
if (allDeps[dep]) {
result.keyDeps.push(label);
}
}
// TypeScript detection
if (allDeps['typescript']) {
result.devTools.push('TypeScript');
// Check tsconfig for strict mode
const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
if (fs.existsSync(tsconfigPath)) {
try {
const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf-8'));
if (tsconfig.compilerOptions?.strict) {
result.devTools.push('TypeScript (strict)');
}
} catch (err) {
// Ignore parse errors
}
}
}
// ESLint
if (allDeps['eslint'] || fs.existsSync(path.join(projectRoot, '.eslintrc.js'))) {
result.devTools.push('ESLint');
}
// Prettier
if (allDeps['prettier'] || fs.existsSync(path.join(projectRoot, '.prettierrc'))) {
result.devTools.push('Prettier');
}
// Extract important scripts
const scripts = pkg.scripts || {};
const importantScripts = ['dev', 'build', 'start', 'test', 'lint', 'format', 'preview'];
for (const script of importantScripts) {
if (scripts[script]) {
result.scripts[script] = scripts[script];
}
}
// Node version
if (pkg.engines?.node) {
result.nodeVersion = pkg.engines.node;
}
// Package manager detection
if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {
result.packageManager = 'pnpm';
} else if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) {
result.packageManager = 'yarn';
} else if (fs.existsSync(path.join(projectRoot, 'package-lock.json'))) {
result.packageManager = 'npm';
} else if (fs.existsSync(path.join(projectRoot, 'bun.lockb'))) {
result.packageManager = 'bun';
}
logDebug(LOG_PREFIX, `Analyzed tech stack: ${result.frameworks.join(', ')}`);
return result;
} catch (err) {
logDebug(LOG_PREFIX, `Error reading package.json: ${err.message}`);
return null;
}
}
/**
* Analyze project directory structure.
*/
function analyzeProjectStructure(projectRoot) {
const structure = {
type: 'unknown',
patterns: [],
keyDirectories: [],
entryPoints: []
};
// Check for common patterns
if (fs.existsSync(path.join(projectRoot, 'app'))) {
structure.patterns.push('App Router (Next.js 13+)');
structure.keyDirectories.push('app/');
}
if (fs.existsSync(path.join(projectRoot, 'pages'))) {
structure.patterns.push('Pages Router');
structure.keyDirectories.push('pages/');
}
if (fs.existsSync(path.join(projectRoot, 'src'))) {
structure.keyDirectories.push('src/');
// Check src subdirectories
const srcSubdirs = ['components', 'hooks', 'lib', 'utils', 'services', 'api', 'store', 'types', 'styles'];
for (const subdir of srcSubdirs) {
if (fs.existsSync(path.join(projectRoot, 'src', subdir))) {
structure.keyDirectories.push(`src/${subdir}/`);
}
}
}
if (fs.existsSync(path.join(projectRoot, 'components'))) {
structure.keyDirectories.push('components/');
}
if (fs.existsSync(path.join(projectRoot, 'lib'))) {
structure.keyDirectories.push('lib/');
}
if (fs.existsSync(path.join(projectRoot, 'public'))) {
structure.keyDirectories.push('public/');
}
// API routes
const apiPaths = [
path.join(projectRoot, 'app', 'api'),
path.join(projectRoot, 'pages', 'api'),
path.join(projectRoot, 'src', 'app', 'api')
];
for (const apiPath of apiPaths) {
if (fs.existsSync(apiPath)) {
structure.patterns.push('API Routes');
break;
}
}
// Monorepo detection
if (fs.existsSync(path.join(projectRoot, 'packages')) ||
fs.existsSync(path.join(projectRoot, 'apps'))) {
structure.type = 'monorepo';
structure.patterns.push('Monorepo');
} else {
structure.type = 'single-package';
}
// Docker
if (fs.existsSync(path.join(projectRoot, 'Dockerfile')) ||
fs.existsSync(path.join(projectRoot, 'docker-compose.yml'))) {
structure.patterns.push('Docker');
}
// Entry points
const entryFiles = [
'app/page.tsx', 'app/page.jsx', 'pages/index.tsx', 'pages/index.jsx',
'src/index.ts', 'src/index.js', 'src/main.ts', 'src/main.tsx',
'index.ts', 'index.js'
];
for (const entry of entryFiles) {
if (fs.existsSync(path.join(projectRoot, entry))) {
structure.entryPoints.push(entry);
}
}
return structure;
}
/**
* Find any active development plan.
*/
function findActivePlan(projectRoot) {
const maestroDir = getMaestroDir(projectRoot);
const planFiles = ['task.md', 'development_plan.md', 'implementation_plan.md'];
for (const planFile of planFiles) {
const planPath = path.join(maestroDir, planFile);
if (fs.existsSync(planPath)) {
return planPath;
}
}
return null;
}
/**
* Build context message from available files.
*/
function buildContextMessage(projectRoot) {
const contextParts = [];
// Check for brain.jsonl (Cross-session memory)
const brainSummary = formatBrainForContext(projectRoot);
if (brainSummary) {
contextParts.push('## 🧠 Long-Term Project Memory');
contextParts.push(brainSummary);
logDebug(LOG_PREFIX, 'Loaded brain.jsonl summary');
}
// Check for active plan
const planPath = findActivePlan(projectRoot);
if (planPath) {
const planContent = readFileSafe(planPath, 30000);
if (planContent) {
const planName = path.basename(planPath);
contextParts.push(`\n## 📋 Active Plan: ${planName}`);
const lines = planContent.split('\n');
if (lines.length > 100) {
contextParts.push(`(Truncated - ${lines.length} lines)`);
contextParts.push(lines.slice(0, 100).join('\n'));
} else {
contextParts.push(planContent);
}
logDebug(LOG_PREFIX, `Loaded plan: ${planName}`);
}
}
return contextParts.length > 0 ? contextParts.join('\n') : null;
}
/**
* Main hook entry point.
*/
async function main() {
const projectRoot = findProjectRoot();
const maestroDir = ensureMaestroDir(projectRoot);
logDebug(LOG_PREFIX, '='.repeat(60));
logDebug(LOG_PREFIX, 'SESSION START HOOK TRIGGERED');
logDebug(LOG_PREFIX, `Working Directory: ${projectRoot}`);
logDebug(LOG_PREFIX, `Maestro Directory: ${maestroDir}`);
try {
// Read hook input
const hookInput = await readStdin();
logDebug(LOG_PREFIX, `Hook input: source=${hookInput.source}, model=${hookInput.model}`);
// COMPACT SUMMARY PERSISTENCE:
// If we're starting after a compaction (matcher='compact' or source='compact'),
// the summary is in the transcript. Let's grab it.
const isCompaction = hookInput.source === 'compact' ||
hookInput.source === 'resume' ||
hookInput.matcher === 'compact' ||
hookInput.matcher === 'resume';
if (isCompaction) {
const transcriptPath = hookInput.transcriptPath || hookInput.transcript_path || hookInput.transcript;
if (transcriptPath) {
logDebug(LOG_PREFIX, `Detected compact/resume session. Attempting summary capture...`);
// RETRY LOOP: Windows I/O and Claude CLI write timing can be tricky.
// We try 3 times with 1s intervals.
let summary = null;
for (let attempt = 1; attempt <= 3; attempt++) {
logDebug(LOG_PREFIX, `Capture attempt ${attempt}/3...`);
summary = extractLastSummary(transcriptPath);
if (summary) break;
if (attempt < 3) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
if (summary) {
writeCompactToBrain(summary, projectRoot);
logDebug(LOG_PREFIX, 'Summary captured and persisted to brain.jsonl');
} else {
logDebug(LOG_PREFIX, 'Failed to capture summary after 3 attempts.');
}
}
}
// STALE CONTEXT GUARD: Detect if project is empty (Treat as Black Slate)
const rootEntries = fs.readdirSync(projectRoot);
const hasProjectFiles = rootEntries.some(e => !['.git', '.maestro', '.claude'].includes(e));
if (!hasProjectFiles) {
logDebug(LOG_PREFIX, 'Project directory empty (except meta) - treated as BLACK SLATE.');
const ts = Date.now();
const maestroDir = getMaestroDir(projectRoot);
const filesToPurge = ['brain.jsonl'];
for (const file of filesToPurge) {
const filePath = path.join(maestroDir, file);
if (fs.existsSync(filePath)) {
const stalePath = path.join(maestroDir, `${file}.stale.${ts}`);
fs.renameSync(filePath, stalePath);
logDebug(LOG_PREFIX, `Purged stale file: ${file} -> ${path.basename(stalePath)}`);
}
}
const output = {
type: 'session_context',
systemMessage: `
# 🎩 MAESTRO: BLACK SLATE PROJECT
This project directory is effectively EMPTY (except for meta-folders like .git).
Existing project memory has been PURGED to prevent stale context retrieval.
**INSTRUCTIONS**:
1. Treat this as a **FRESH START**.
2. Do NOT attempt to recover context from 'git log' or old memory files.
3. Architecture, tech stack, and goals should be defined FROM SCRATCH based on user's new request.
`
};
outputJson(output);
return;
}
// 1. Analyze tech stack if needed (package.json changed or first run)
if (shouldReanalyzeTech(projectRoot)) {
logDebug(LOG_PREFIX, 'Tech stack analysis triggered');
const techInfo = analyzePackageJson(projectRoot);
const structure = analyzeProjectStructure(projectRoot);
if (techInfo) {
writeTechToBrain(techInfo, structure, projectRoot);
saveTechHash(projectRoot);
logDebug(LOG_PREFIX, 'Tech stack info written to brain.jsonl');
}
} else {
logDebug(LOG_PREFIX, 'Tech stack already analyzed, skipping');
}
// 2. Build context from LTM (brain.jsonl) and Plans
const context = buildContextMessage(projectRoot);
if (context) {
const output = {
type: 'session_context',
systemMessage: `
# 🎩 MAESTRO SESSION CONTEXT
The following context was loaded from project memory and active plans. This represents the **LONG-TERM MEMORY** of the project.
${context}
---
**CRITICAL INSTRUCTIONS**:
1. **Memory Continuity**: Review the "Recent Compact Summaries" above. These contain the distilled history of previous interactions. Use them to maintain seamless continuity.
2. **Context Awareness**: The Tech Stack and Architecture sections define the playground. Use these for high-accuracy searches and implementation decisions.
3. **Task Tracking**: If \`task.md\` exists, it is the source of truth for current progress. Always keep it updated.
4. **Communication**: Mirror the user's language mirroring protocol as defined in the Architect roles.
`
};
outputJson(output);
logDebug(LOG_PREFIX, 'Context injected successfully');
} else {
outputJson({});
logDebug(LOG_PREFIX, 'No context files found');
}
logDebug(LOG_PREFIX, 'Session start hook completed');
} catch (err) {
// Always output valid JSON even on error
const errorOutput = {
type: 'session_context',
systemMessage: `⚠️ Hook Error: ${err.message}\n\nStack: ${err.stack}`
};
outputJson(errorOutput);
}
}
main();
#!/usr/bin/env node
/**
* Stop Hook for Maestro Plugin (Ralph Wiggum Controller)
* Controls whether Claude can exit or must continue iterating.
*
* @event Stop, SubagentStop
*/
const {
findProjectRoot,
logDebug,
readStdin,
outputJson
} = require('./lib/utils');
const Ralph = require('./lib/ralph');
const LOG_PREFIX = '[STOP]';
/**
* Main hook entry point.
*/
async function main() {
const projectRoot = findProjectRoot();
logDebug(LOG_PREFIX, '='.repeat(60));
logDebug(LOG_PREFIX, 'STOP HOOK TRIGGERED');
try {
const hookInput = await readStdin();
const inputData = JSON.stringify(hookInput);
// Check if this is a SubagentStop event
const hookEvent = hookInput.hookEventName || '';
const isSubagentStop = hookEvent === 'SubagentStop' ||
inputData.toLowerCase().includes('subagent');
if (isSubagentStop) {
logDebug(LOG_PREFIX, 'SUBAGENT STOP detected - allowing subagent to complete');
outputJson({});
process.exit(0);
return;
}
// Check Ralph Wiggum iteration state
const decision = Ralph.getBlockDecision();
if (!decision.block) {
// Allow exit
logDebug(LOG_PREFIX, `Exit allowed: ${decision.reason}`);
// Cleanup if completed
if (decision.completed) {
logDebug(LOG_PREFIX, 'Ralph Wiggum completed - cleaning up state');
Ralph.clearState();
}
outputJson({});
process.exit(0);
return;
}
// Block exit - Ralph wants more iterations
logDebug(LOG_PREFIX, `Exit BLOCKED: ${decision.reason} (${decision.current}/${decision.max})`);
// Build continuation message
const continuationMessage = `
🔄 RALPH WIGGUM 2.0: ELITE PERSISTENCE ACTIVE
## 📊 Iteration Status
**Progress:** ${decision.current} / ${decision.max}
## ⚠️ TASK COMPLETION BLOCKED
Ralph Wiggum requires more iterations to ensure quality standards are met.
### Next Steps:
1. Continue testing and fixing issues
2. Run tests again to verify fixes
3. Check verification matrix coverage
4. Ensure all critical tests pass
The task will remain blocked until:
- All tests pass OR
- Maximum iterations reached OR
- Manual completion signal received
---
**To manually complete:** Create .maestro/ralph.complete file
**To stop early:** Delete .maestro/ralph.active file
`;
// Block the exit with continuation prompt
outputJson({
block: true,
message: continuationMessage.trim(),
iteration: decision.current,
maxIterations: decision.max
});
process.exit(0);
} catch (err) {
logDebug(LOG_PREFIX, `Error: ${err.message}`);
outputJson({});
}
}
main();
MIT License
Copyright (c) 2025-2026 xenitV1 (https://github.com/xenitV1)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
"name": "maestro",
"version": "0.6.0",
"description": "Elite AI Development Orchestrator for Claude Code CLI. Provides specialized agents, modular skills, intelligent hooks, and persistent memory systems.",
"author": {
"name": "xenitV1",
"url": "https://github.com/xenitV1",
"twitter": "https://x.com/xenit_v0"
},
"license": "MIT",
"keywords": [
"claude-code",
"ai-orchestration",
"development-tools",
"memory-systems",
"qa-automation",
"ralph-wiggum"
],
"repository": {
"type": "git",
"url": "https://github.com/xenitV1/claude-code-maestro"
},
"engines": {
"node": ">=18.0.0"
},
"type": "commonjs",
"private": true
}Maestro: Yapay Zeka Geliştirme Orkestratörü
Claude Code CLI için elit seviye orkestrasyon çerçevesi. Özelleşmiş ajanlar, modüler beceriler, akıllı hook sistemi ve kalıcı bellek sistemleri aracılığıyla yapay zeka geliştirmeyi güçlendirir.
Versiyon: 0.6.0
Yazar: xenitV1 • X/Twitter
Felsefe: "Nasıl'dan önce Neden. Mimari, uygulamadan önce gelir."
Hızlı Başlangıç
Kurulum
Maestro bir Claude Code eklentisi (plugin) olarak dağıtılır. Kurmak için önce repoyu bir "marketplace" olarak eklemeniz gerekir:
# 1. Maestro reposunu marketplace olarak ekleyin
/plugin marketplace add xenitV1/claude-code-maestro
# 2. Maestro eklentisini kurun
/plugin install maestro@xenitV1-claude-code-maestroÖnkoşullar
- Node.js 18+ (hook sistemi için gereklidir)
- Claude Code CLI
Kullanım
Maestro bir eklenti (plugin) olduğu için komutları isim alanı (namespace) gerektirir. /maestro:komut formatını kullanın.
# Basic orchestration
/maestro your task description
# With Ralph Wiggum (autonomous iterations)
/maestro fix bugs and improve code. ralph 5 iterations
# Design mode
/maestro design new authentication system
# Plan mode
/maestro plan implement user dashboard
# Use the Grandmaster agent directly
/agent:grandmasterMimari
┌─────────────────────────────────────────────────────────────────────┐
│ MAESTRO SİSTEMİ │
├─────────────────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────────┐ ┌─────────────────────────┐ │
│ │ /maestro │───▶│ grandmaster │───▶│ BECERİLER │ │
│ │ komutu │ │ ajanı │ │ (frontend, backend, │ │
│ └──────────┘ └──────────────┘ │ tdd, debug, vb.) │ │
│ │ └─────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ HOOK SİSTEMİ │ │
│ │ SessionStart │ PostToolUse │ Stop │ PreCompact │ vb. │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ UZUN SÜRELİ BELLEK (LTM) │ │
│ │ (brain.jsonl) │ │
│ └──────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘Proje Yapısı
maestro/
├── .claude-plugin/
│ └── plugin.json # Plugin manifestosu
├── agents/
│ └── grandmaster.md # Merkezi orkestratör ajan
├── commands/
│ └── maestro.md # /maestro slash komutu
├── hooks/
│ ├── hooks.json # Hook konfigürasyonu
│ ├── lib/ # Paylaşılan JS yardımcıları
│ │ ├── utils.js # Platformlar arası yardımcılar
│ │ ├── brain.js # LTM işlemleri
│ │ └── ralph.js # Ralph durum yönetimi
│ ├── session-start.js # Tech stack tespiti + LTM enjeksiyonu
│ ├── brain-sync.js # LTM senkronizasyonu (PostToolUse)
│ ├── stop.js # Ralph Wiggum iterasyonu
│ ├── ralph.js # QA zorlaması
│ ├── sentinel.js # Değişiklik algılama
│ └── pre-maestro.js # Beceri önerisi
├── skills/
│ ├── clean-code/ # Kod kalite standartları
│ ├── frontend-design/ # Elit UI/UX
│ ├── backend-design/ # API & Veritabanı kalıpları
│ ├── tdd-mastery/ # Test Odaklı Geliştirme
│ ├── debug-mastery/ # Sistematik hata ayıklama
│ ├── verification-mastery/# Kanıt tabanlı tamamlama
│ ├── brainstorming/ # Tasarım öncelikli metodoloji
│ ├── planning-mastery/ # Uygulama planlama
│ ├── git-worktrees/ # İzole çalışma alanları
│ ├── ralph-wiggum/ # Otonom QA sistemi
│ ├── browser-extension/ # Tarayıcı eklentisi geliştirme
│ └── optimization-mastery/# Performans optimizasyonu
├── package.json # Node.js meta verileri
├── LICENSE # MIT Lisansı
└── README.mdBellek Sistemleri
Uzun Süreli Bellek (brain.jsonl)
Oturumlar arası kalıcı proje bağlamı:
- Tech Stack: Framework'ler, bağımlılıklar, mimari kalıplar
- Kararlar: Alınan önemli mimari kararlar
- Hedefler: Proje amaçları
- Hatalar: Bilinen sorunlar ve engelleyiciler
- Compact Geçmişi: Context sıkıştırması sonrası oturum özetleri
- Dosya Değişiklikleri: Edit ve Create işlemlerinin kaydı
Ralph Wiggum: Otonom QA
Dört Sütunlu Elit QA Sistemi:
| Sütun | Amaç |
|---|---|
| Proactive Gate | Kodlamadan ÖNCE kenar durum tespiti |
| Reflection Loop | Öz-eleştiri ve iyileştirme |
| Verification Matrix | Test kapsama takibi (minimum %80) |
| Circuit Breaker | Duraksama tespiti ve pivot stratejileri |
Aktifleştirme: ralph N iterasyon veya "Ralph Wiggum modu"
Becerilere Genel Bakış
| Beceri | Açıklama |
|---|---|
clean-code | 2025 standartları, SOLID, güvenlik öncelikli |
frontend-design | Atomic Design 2.0, Lovable/v0 standardı |
backend-design | Zero-trust, API sözleşmeleri |
tdd-mastery | Demir Yasa: Koddan önce test |
debug-mastery | 4 fazlı sistematik hata ayıklama |
verification-mastery | Tamamlamadan önce kanıt |
brainstorming | Tasarım öncelikli metodoloji |
planning-mastery | Küçük parçalı görev kırılımı |
git-worktrees | İzole özellik geliştirme |
ralph-wiggum | Otonom QA orkestrasyonu |
optimization-mastery | Performans, INP, kısmi hidrasyon |
context7 | Upstash üzerinden otomatik kütüphane dokümantasyonu |
browser-extension | Manifest v3, servis çalışanları |
Platform Uyumluluğu
| Platform | Puan | Notlar |
|---|---|---|
| Claude Code CLI | ⭐⭐⭐⭐⭐ | Doğal ortam, tam işlevsellik |
| Windows | ⭐⭐⭐⭐⭐ | Tam platformlar arası destek |
| macOS | ⭐⭐⭐⭐⭐ | Tam platformlar arası destek |
| Linux | ⭐⭐⭐⭐⭐ | Tam platformlar arası destek |
Çekirdek Protokoller
1. Sokratik Geçit: Varsaymadan önce netleştirici sorular sor 2. Önce Düşün: Karmaşık eylemlerden önce <think> kullan 3. TDD Demir Yasası: Başarısız test olmadan üretim kodu yok 4. Doğrulama: Tamamlama iddialarından önce kanıt 5. Temiz Kod: TODO/FIXME yok, tembel placeholder yok
Teşekkür
Birçok beceri obra/superpowers projesinden ilham alınarak adapte edilmiş ve Maestro orkestrasyon ortamı için ağır bir şekilde optimize edilmiştir:
| Özellik | Kaynak | Maestro Becerisi |
|---|---|---|
| TDD Demir Yasası | superpowers/tdd | tdd-mastery |
| Sistematik Hata Ayıklama | superpowers/debugging | debug-mastery |
| Doğrulama Protokolü | superpowers/verification | verification-mastery |
| Beyin Fırtınası Metodu | superpowers/brainstorming | brainstorming |
| Uygulama Planlama | superpowers/planning | planning-mastery |
| Git Worktrees | superpowers/worktrees | git-worktrees |
Yıldız Geçmişi

Yazar
[xenitV1](https://github.com/xenitV1) tarafından oluşturuldu ve sürdürülüyor.
- GitHub: github.com/xenitV1
- X/Twitter: x.com/xenit_v0
Lisans
MIT Lisansı - Detaylar için LICENSE dosyasına bakınız.
---
Otonom geliştirmenin geleceğini orkestre ediyoruz.
#!/usr/bin/env node
/**
* MAESTRO PERSISTENCE CHECKER (SW Survival Analyst)
*
* Rules:
* 1. Must listen to chrome.alarms.onAlarm for wakeup.
* 2. Must use chrome.storage for state (No globals).
* 3. Service Worker must be in the root or background/ directory.
*/
const fs = require('fs');
const path = require('path');
/**
* Analyzes a Service Worker for persistence patterns.
*/
function checkPersistence(filepath) {
const issues = [];
try {
const content = fs.readFileSync(filepath, 'utf-8');
// RULE 1: Alarm Wakeup
if (!content.includes('chrome.alarms.onAlarm.addListener')) {
issues.push('[PERSISTENCE] Service Worker missing alarm listener. SW will terminate and never wake up.');
}
// RULE 2: Storage usage
if (!content.includes('chrome.storage') && !content.includes('chrome.storage.local')) {
issues.push("[STATE-LOSS] Use of 'chrome.storage' not detected. Persist state to avoid data loss on termination.");
}
// RULE 3: Global Variables (Naive check)
const globalsPattern = /^(?:let|var)\s+\w+\s*=/gm;
const globalsFound = content.match(globalsPattern) || [];
if (globalsFound.length > 2) {
issues.push(`[ARCHITECTURE] ${globalsFound.length} global variables detected. SW globals are ephemeral. Use storage.`);
}
} catch (err) {
issues.push(`[ERROR] Could not read service worker: ${err.message}`);
}
return issues;
}
/**
* Main function.
*/
function main() {
console.log('⚡ STARTING PERSISTENCE INTEGRITY CHECK...');
const target = process.argv[2] || 'background.js';
if (!fs.existsSync(target)) {
console.log(`[SKIP] ${target} not found. Skipping persistence check.`);
return;
}
const issues = checkPersistence(target);
if (issues.length > 0) {
console.log(`\n🚨 PERSISTENCE RISKS DETECTED in ${target}:`);
for (const issue of issues) {
console.log(` ⚠️ ${issue}`);
}
process.exit(1);
} else {
// Write success state
try {
const projectRoot = process.cwd();
const stateDir = path.join(projectRoot, '.maestro');
if (!fs.existsSync(stateDir)) {
fs.mkdirSync(stateDir, { recursive: true });
}
fs.writeFileSync(path.join(stateDir, 'audit.state'), String(Date.now() / 1000));
} catch (err) {
console.log(`[WARN] Could not save audit state: ${err.message}`);
}
console.log('✅ PERSISTENCE VERIFIED: Pulse and State patterns detected.');
process.exit(0);
}
}
main();
Related skills
How it compares
Pick maestro over lightweight session skills when the repository is complex and needs architectural governance plus persistent memory, not just end-of-session cleanup.
FAQ
What lifecycle does maestro enforce?
maestro runs a Plan-Act-Verify lifecycle, orchestrating specialized sub-skills and requiring verification steps before treating agent output as complete on complex repositories.
What is the Brain in maestro?
maestro's Brain is persistent project memory that carries architectural context and decisions across sessions so the agent maintains continuity on large codebases.
Is Maestro safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.