mirror of
https://github.com/kochetkov-ma/claude-brewcode.git
synced 2026-09-14 20:16:41 +08:00
v3.1.0
This commit is contained in:
@@ -92,6 +92,48 @@ export function parseTask(taskPath, cwd = null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// v3 detection: phases/ directory alongside PLAN.md + v3 header format
|
||||
const taskDir = dirname(taskPath);
|
||||
const phasesDir = join(taskDir, 'phases');
|
||||
const hasPhases = existsSync(phasesDir);
|
||||
|
||||
if (hasPhases) {
|
||||
const lines = content.split('\n');
|
||||
if (lines[0]?.startsWith('status:') && lines[1]?.startsWith('current_phase:')) {
|
||||
return parseTaskV3(content);
|
||||
}
|
||||
const derivedCwd = cwd || taskPath.replace(/\/\.claude\/tasks\/.*$/, '');
|
||||
log('warn', '[parseTask]', 'phases/ dir exists but PLAN.md lacks v3 header, falling back to v2', derivedCwd);
|
||||
}
|
||||
|
||||
return parseTaskV2(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse v3 PLAN.md with structured header (status, current_phase, total_phases).
|
||||
* Uses multiline regex for robustness against line order variations.
|
||||
* @param {string} content - PLAN.md file content
|
||||
* @returns {Object} Parsed task with status, currentPhase, totalPhases, content
|
||||
*/
|
||||
function parseTaskV3(content) {
|
||||
const statusMatch = content.match(/^status:\s*(.+)/m);
|
||||
const status = statusMatch?.[1]?.trim() || 'pending';
|
||||
|
||||
const currentPhaseMatch = content.match(/^current_phase:\s*(\d+)/m);
|
||||
const currentPhase = currentPhaseMatch ? parseInt(currentPhaseMatch[1]) : 0;
|
||||
|
||||
const totalPhasesMatch = content.match(/^total_phases:\s*(\d+)/m);
|
||||
const totalPhases = totalPhasesMatch ? parseInt(totalPhasesMatch[1]) : 0;
|
||||
|
||||
return { status, currentPhase, totalPhases, content };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse v2 PLAN.md with phase headers and checkbox counting
|
||||
* @param {string} content - PLAN.md file content
|
||||
* @returns {Object} Parsed task with status, currentPhase, totalPhases, content
|
||||
*/
|
||||
function parseTaskV2(content) {
|
||||
// Extract status
|
||||
const statusMatch = content.match(/^status:\s*(.+)$/m);
|
||||
const status = statusMatch?.[1]?.trim() || 'pending';
|
||||
@@ -123,12 +165,7 @@ export function parseTask(taskPath, cwd = null) {
|
||||
|
||||
const totalPhases = phaseHeaders.length || 1;
|
||||
|
||||
return {
|
||||
status,
|
||||
currentPhase,
|
||||
totalPhases,
|
||||
content
|
||||
};
|
||||
return { status, currentPhase, totalPhases, content };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,7 +222,7 @@ const DEFAULT_CONFIG = {
|
||||
system: [
|
||||
'bc-coordinator', 'bc-knowledge-manager',
|
||||
'brewcode:bc-coordinator', 'brewcode:bc-knowledge-manager',
|
||||
'bc-auto-sync-processor', 'brewcode:bc-auto-sync-processor',
|
||||
'bd-auto-sync-processor', 'brewcode:bd-auto-sync-processor',
|
||||
'bc-grepai-configurator', 'brewcode:bc-grepai-configurator',
|
||||
'Explore', 'Plan', 'Bash', 'general-purpose',
|
||||
'claude-code-guide', 'skill-creator', 'agent-creator',
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* - Detects bc-coordinator and binds session to lock file
|
||||
* - Reminds to call bc-coordinator after work agents complete
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import {
|
||||
readStdin,
|
||||
output,
|
||||
@@ -82,14 +84,27 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return 2-step post-agent protocol
|
||||
// Return post-agent protocol (branched on success/failure, extended for v3 Task API)
|
||||
const agentName = String(subagentType || '').toUpperCase();
|
||||
const failed = tool_result?.is_error === true;
|
||||
const status = failed ? 'FAILED' : 'DONE';
|
||||
const taskDir = lock.task_path ? path.dirname(path.join(cwd, lock.task_path)) : '';
|
||||
const isV3 = taskDir && fs.existsSync(path.join(taskDir, 'phases'));
|
||||
|
||||
let message;
|
||||
if (failed) {
|
||||
const failBase = `${agentName} FAILED -> 1. Retry once with same agent 2. If retry fails: TaskUpdate(taskId, status="failed"), apply Escalation 3. Do NOT write report, do NOT call bc-coordinator`;
|
||||
const failV3Suffix = `\n4. Persist failure to KNOWLEDGE.jsonl\n5. Check for blocked dependents -> cascade failure\n⛔ DO NOT read phases/ files — they are for agents only.`;
|
||||
message = isV3 ? failBase + failV3Suffix : failBase;
|
||||
} else {
|
||||
const successBase = `${agentName} DONE -> 1. WRITE report 2. CALL bc-coordinator NOW`;
|
||||
const successV3Suffix = `\n3. TaskUpdate(taskId, status="completed")\n4. TaskList() -> find next ready task\n⛔ DO NOT read phases/ files — they are for agents only.`;
|
||||
message = isV3 ? successBase + successV3Suffix : successBase;
|
||||
}
|
||||
|
||||
output({
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PostToolUse',
|
||||
additionalContext: `${agentName} ${status} -> 1. WRITE report 2. CALL bc-coordinator NOW`
|
||||
additionalContext: message
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* ============================================================================
|
||||
*/
|
||||
import { existsSync, readdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { join, dirname } from 'path';
|
||||
import {
|
||||
readStdin,
|
||||
output,
|
||||
@@ -70,8 +70,9 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// If task is finished, allow compact without validation
|
||||
if (task.status === 'finished') {
|
||||
// If task is in a terminal state, allow compact without handoff logic
|
||||
const TERMINAL_STATUSES = new Set(['finished', 'failed', 'cancelled', 'error']);
|
||||
if (TERMINAL_STATUSES.has(task.status)) {
|
||||
output({ continue: true });
|
||||
return;
|
||||
}
|
||||
@@ -93,7 +94,7 @@ async function main() {
|
||||
|
||||
// If validation issues, warn but continue (don't block compact)
|
||||
if (validationIssues.length > 0) {
|
||||
log('warn', '[pre-compact]', `Validation warnings: ${validationIssues.join('; ')}`, cwd, session_id);
|
||||
log('debug', '[pre-compact]', `Validation warnings (agent may still be executing): ${validationIssues.join('; ')}`, cwd, session_id);
|
||||
// Still continue - better to compact than crash
|
||||
}
|
||||
|
||||
@@ -122,12 +123,20 @@ async function main() {
|
||||
|
||||
log('info', '[pre-compact]', `Handoff to phase ${task.currentPhase}`, cwd, session_id);
|
||||
|
||||
// Detect v3: phases/ directory exists inside the task dir
|
||||
const taskDir = dirname(taskPath);
|
||||
const isV3 = existsSync(join(taskDir, 'phases'));
|
||||
|
||||
const systemMessage = isV3
|
||||
? `brewcode: compact handoff, phase ${task.currentPhase}/${task.totalPhases}. After compact: 1) TaskList() for current task state 2) Read PLAN.md for protocol 3) DO NOT read phases/ — they are for agents 4) Continue with current in_progress or next pending task 5) WRITE report → CALL coordinator after EVERY agent`
|
||||
: `brewcode: compact handoff, phase ${task.currentPhase}/${task.totalPhases}`;
|
||||
|
||||
// Return continue to allow compact
|
||||
// systemMessage = short status for user
|
||||
// session-start.mjs (source='compact') handles Claude re-read instruction via additionalContext
|
||||
output({
|
||||
continue: true,
|
||||
systemMessage: `brewcode: compact handoff, phase ${task.currentPhase}/${task.totalPhases}`
|
||||
systemMessage
|
||||
});
|
||||
} catch (error) {
|
||||
log('error', '[pre-compact]', `Error: ${error.message}`, cwd, session_id);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* PreToolUse hook for Task tool
|
||||
* - Injects grepai reminder for ALL agents (when .grepai/ exists)
|
||||
* - Injects ## K knowledge into sub-agent prompts (brewcode only)
|
||||
* - Injects v3 task context (phase reminder + paths) when phases/ exists
|
||||
*/
|
||||
import {
|
||||
readStdin,
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
} from './lib/utils.mjs';
|
||||
import { readKnowledge, compressKnowledge } from './lib/knowledge.mjs';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { join, dirname } from 'path';
|
||||
|
||||
const GREPAI_REMINDER = 'grepai: USE grepai_search FIRST for code exploration';
|
||||
|
||||
@@ -115,7 +116,28 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Inject constraints for non-system agents
|
||||
// 3. Inject v3 task context (phase reminder + paths) for non-system agents
|
||||
if (lock && lock.task_path) {
|
||||
const taskDir = dirname(join(cwd, lock.task_path));
|
||||
const phasesDir = join(taskDir, 'phases');
|
||||
|
||||
if (existsSync(phasesDir)) {
|
||||
const artifactsDir = join(taskDir, 'artifacts');
|
||||
const taskContext = [
|
||||
'## Task Context',
|
||||
`Task dir: ${taskDir}`,
|
||||
`Artifacts: ${artifactsDir}`,
|
||||
'',
|
||||
'> ⛔ READ the phases/ file referenced in your task description FIRST before doing any work.'
|
||||
].join('\n');
|
||||
|
||||
updatedPrompt = `${taskContext}\n\n${updatedPrompt}`;
|
||||
modified = true;
|
||||
log('debug', '[pre-task]', `Injecting v3 task context for ${subagentType}`, cwd, session_id);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Inject constraints for non-system agents
|
||||
if (config.constraints?.enabled !== false && lock && lock.task_path) {
|
||||
const taskPath = join(cwd, lock.task_path);
|
||||
let taskContent = null;
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
*
|
||||
* Cleanup: /brewcode:teardown removes .claude/plans/ directory
|
||||
*/
|
||||
import { readStdin, output, log, getActiveTaskPath } from './lib/utils.mjs';
|
||||
import { readStdin, output, log, getActiveTaskPath, getLock } from './lib/utils.mjs';
|
||||
import { readdirSync, statSync, mkdirSync, symlinkSync, unlinkSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { join, dirname } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
const PLAN_FRESHNESS_MS = 60_000;
|
||||
@@ -95,8 +95,28 @@ async function main() {
|
||||
? `BC_PLUGIN_ROOT=${pluginRoot}\nbrewcode: active | session: ${sessionShort}`
|
||||
: `brewcode: active | session: ${sessionShort}`;
|
||||
|
||||
if (source === 'compact' && cwd && getActiveTaskPath(cwd)) {
|
||||
context += '\n\n[HANDOFF after compact] Re-read PLAN.md and KNOWLEDGE.jsonl, then continue current phase.';
|
||||
if (cwd) {
|
||||
const lock = getLock(cwd);
|
||||
if (lock?.task_path && (!lock.session_id || lock.session_id === session_id)) {
|
||||
const taskDir = dirname(join(cwd, lock.task_path));
|
||||
const isV3 = existsSync(join(taskDir, 'phases'));
|
||||
|
||||
if (source === 'compact') {
|
||||
if (isV3) {
|
||||
context += '\n\n[HANDOFF after compact] 1) TaskList() for current task state 2) Read PLAN.md for protocol 3) DO NOT read phases/ — they are for agents 4) Continue with current in_progress or next pending task 5) WRITE report -> CALL coordinator after EVERY agent';
|
||||
} else {
|
||||
context += '\n\n[HANDOFF after compact] Re-read PLAN.md and KNOWLEDGE.jsonl, then continue current phase.';
|
||||
}
|
||||
}
|
||||
|
||||
if (isV3) {
|
||||
context += '\n\nbrewcode v3: You work through Task API. Call TaskList() to get current task state. DO NOT read phases/ files.';
|
||||
log('info', '[session-start]', `v3 task detected at ${taskDir}, injected Task API reminder`, cwd, session_id);
|
||||
}
|
||||
} else if (source === 'compact' && getActiveTaskPath(cwd)) {
|
||||
// Fallback: lock missing/mismatch but TASK.md reference exists (v2 task without lock)
|
||||
context += '\n\n[HANDOFF after compact] Re-read PLAN.md and KNOWLEDGE.jsonl, then continue current phase.';
|
||||
}
|
||||
}
|
||||
|
||||
output({
|
||||
|
||||
@@ -81,7 +81,7 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get task path from lock
|
||||
// Defense-in-depth: getLock() already validates task_path, this is a backup check
|
||||
const taskPath = lock.task_path;
|
||||
if (taskPath && !validateTaskPath(taskPath)) {
|
||||
log('warn', '[stop]', `Invalid task_path in lock: ${taskPath}`, cwd, session_id);
|
||||
@@ -141,8 +141,7 @@ Emergency exit: rm .claude/tasks/*_task/.lock`,
|
||||
});
|
||||
} catch (error) {
|
||||
log('error', '[stop]', `Error: ${error.message}`, cwd, session_id);
|
||||
try { deleteLock(cwd); } catch (_) {}
|
||||
// On error, allow stop (don't trap user)
|
||||
// On error, allow stop but preserve lock for recovery (user can rm .lock manually)
|
||||
output({});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user