This commit is contained in:
kochetkov-ma
2026-02-28 10:53:52 +00:00
parent 23655fe788
commit 43953b56d7
81 changed files with 7316 additions and 3084 deletions
+44 -7
View File
@@ -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',
+18 -3
View File
@@ -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) {
+14 -5
View File
@@ -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);
+24 -2
View File
@@ -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;
+24 -4
View File
@@ -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({
+2 -3
View File
@@ -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({});
}
}