diff --git a/src/agents/sisyphus.ts b/src/agents/sisyphus.ts index c1a03eaa1..959a22fad 100644 --- a/src/agents/sisyphus.ts +++ b/src/agents/sisyphus.ts @@ -160,7 +160,44 @@ const SISYPHUS_PHASE2B_PRE_IMPLEMENTATION = `## Phase 2B - Implementation ### Pre-Implementation: 1. If task has 2+ steps → Create todo list IMMEDIATELY, IN SUPER DETAIL. No announcements—just create it. 2. Mark current task \`in_progress\` before starting -3. Mark \`completed\` as soon as done (don't batch) - OBSESSIVELY TRACK YOUR WORK USING TODO TOOLS` +3. Mark \`completed\` as soon as done (don't batch) - OBSESSIVELY TRACK YOUR WORK USING TODO TOOLS + +### Parallel Implementation (CONTINUE using background agents!) + +**Don't stop parallelizing just because you're implementing!** + +| Task Type | Parallelize? | How | +|-----------|--------------|-----| +| Frontend UI changes | YES | \`background_task(agent="frontend-ui-ux-engineer", ...)\` | +| Documentation | YES | \`background_task(agent="document-writer", ...)\` | +| Independent modules/files | YES | Work on A while agent handles B | +| Pattern verification | YES | Fire explore agents to verify similar files | +| Research while coding | YES | Fire librarian for docs you'll need | +| Sequential dependencies | NO | Wait for dependency, then continue | + +**Example - Feature with UI + Backend + Docs:** +\`\`\`typescript +// PARALLEL: Fire agents for delegatable work +background_task(agent="frontend-ui-ux-engineer", prompt="Build settings page UI...") +background_task(agent="document-writer", prompt="Document the settings API...") + +// MEANWHILE: Sisyphus implements backend (your direct work) +Edit(file="src/api/settings.ts", ...) +Edit(file="src/api/settings.ts", ...) + +// COLLECT: When you need their results +background_output(task_id="ui-task-id") +background_output(task_id="docs-task-id") + +// VERIFY: Review and integrate +\`\`\` + +**Implementation Parallelism Rules:** +1. **YOUR work** = one \`in_progress\` TODO at a time +2. **DELEGATED work** = unlimited parallel background agents +3. **Before each TODO**: Ask "Can a specialist agent do this faster?" +4. **Track delegations**: Note which TODOs are running in background +5. **Collect before verify**: Get all background results before marking complete` const SISYPHUS_DELEGATION_PROMPT_STRUCTURE = `### Delegation Prompt Structure (MANDATORY - ALL 7 sections): diff --git a/src/features/opencode-skill-loader/async-loader.ts b/src/features/opencode-skill-loader/async-loader.ts index bfb1e7fcc..455bda766 100644 --- a/src/features/opencode-skill-loader/async-loader.ts +++ b/src/features/opencode-skill-loader/async-loader.ts @@ -80,7 +80,7 @@ export async function loadSkillFromPathAsync( const content = await readFile(skillPath, "utf-8") const { data, body, parseError } = parseFrontmatter(content) if (parseError) return null - + const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content) const mcpJsonMcp = await loadMcpJsonFromDirAsync(resolvedPath) const mcpConfig = mcpJsonMcp || frontmatterMcp @@ -133,7 +133,10 @@ function parseAllowedTools(allowedTools: string | undefined): string[] | undefin return allowedTools.split(/\s+/).filter(Boolean) } -export async function discoverSkillsInDirAsync(skillsDir: string): Promise { +export async function discoverSkillsInDirAsync( + skillsDir: string, + scope: SkillScope = "opencode-project" +): Promise { try { const entries = await readdir(skillsDir, { withFileTypes: true }) @@ -149,12 +152,12 @@ export async function discoverSkillsInDirAsync(skillsDir: string): Promise { port.on("message", async (input: WorkerInput) => { try { - const results = await Promise.all( - input.dirs.map(dir => discoverSkillsInDirAsync(dir)) - ) + const scope = input.scopes[0] ?? "opencode-project" + const results = await Promise.all( + input.dirs.map((dir) => discoverSkillsInDirAsync(dir, scope)) + ) const skills = results.flat() diff --git a/src/features/opencode-skill-loader/loader.test.ts b/src/features/opencode-skill-loader/loader.test.ts index b41595757..e92b55db0 100644 --- a/src/features/opencode-skill-loader/loader.test.ts +++ b/src/features/opencode-skill-loader/loader.test.ts @@ -150,11 +150,10 @@ Skill body. try { const skills = discoverSkills({ includeClaudeCodePaths: false }) - // #then - when YAML fails, skill uses directory name as fallback + // #then - malformed frontmatter should skip the skill entirely const skill = skills.find(s => s.name === "bad-yaml-skill") - expect(skill).toBeDefined() - expect(skill?.mcpConfig).toBeUndefined() + expect(skill).toBeUndefined() } finally { process.chdir(originalCwd) } diff --git a/src/features/opencode-skill-loader/loader.ts b/src/features/opencode-skill-loader/loader.ts index 9e2eb64fc..ba6ca4e57 100644 --- a/src/features/opencode-skill-loader/loader.ts +++ b/src/features/opencode-skill-loader/loader.ts @@ -67,7 +67,8 @@ function loadSkillFromPath( ): LoadedSkill | null { try { const content = readFileSync(skillPath, "utf-8") - const { data } = parseFrontmatter(content) + const { data, parseError } = parseFrontmatter(content) + if (parseError) return null const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content) const mcpJsonMcp = loadMcpJsonFromDir(resolvedPath) const mcpConfig = mcpJsonMcp || frontmatterMcp @@ -84,8 +85,12 @@ function loadSkillFromPath( load: async () => { if (!lazyContent.loaded) { const fileContent = await fs.readFile(skillPath, "utf-8") - const { body } = parseFrontmatter(fileContent) - lazyContent.content = ` + const { body, parseError } = parseFrontmatter(fileContent) + + if (parseError) { + lazyContent.content = "" + } else { + lazyContent.content = ` Base directory for this skill: ${resolvedPath}/ File references (@path) in this skill are relative to this directory. @@ -95,6 +100,8 @@ ${body.trim()} $ARGUMENTS ` + } + lazyContent.loaded = true } return lazyContent.content! @@ -137,7 +144,8 @@ async function loadSkillFromPathAsync( ): Promise { try { const content = await fs.readFile(skillPath, "utf-8") - const { data } = parseFrontmatter(content) + const { data, parseError } = parseFrontmatter(content) + if (parseError) return null const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content) const mcpJsonMcp = loadMcpJsonFromDir(resolvedPath) const mcpConfig = mcpJsonMcp || frontmatterMcp @@ -153,8 +161,12 @@ async function loadSkillFromPathAsync( load: async () => { if (!lazyContent.loaded) { const fileContent = await fs.readFile(skillPath, "utf-8") - const { body } = parseFrontmatter(fileContent) - lazyContent.content = ` + const { body, parseError } = parseFrontmatter(fileContent) + + if (parseError) { + lazyContent.content = "" + } else { + lazyContent.content = ` Base directory for this skill: ${resolvedPath}/ File references (@path) in this skill are relative to this directory. @@ -164,6 +176,8 @@ ${body.trim()} $ARGUMENTS ` + } + lazyContent.loaded = true } return lazyContent.content! diff --git a/src/features/skill-mcp-manager/env-cleaner.ts b/src/features/skill-mcp-manager/env-cleaner.ts index 9a3faba79..71f21c866 100644 --- a/src/features/skill-mcp-manager/env-cleaner.ts +++ b/src/features/skill-mcp-manager/env-cleaner.ts @@ -13,7 +13,7 @@ export function createCleanMcpEnvironment( const cleanEnv: Record = {} for (const [key, value] of Object.entries(process.env)) { - if (value === undefined) continue + if (value === undefined || value === "undefined") continue const shouldExclude = EXCLUDED_ENV_PATTERNS.some((pattern) => pattern.test(key)) if (!shouldExclude) { diff --git a/src/hooks/agent-usage-reminder/constants.ts b/src/hooks/agent-usage-reminder/constants.ts index 26e6c8750..636ef8283 100644 --- a/src/hooks/agent-usage-reminder/constants.ts +++ b/src/hooks/agent-usage-reminder/constants.ts @@ -26,6 +26,9 @@ export const AGENT_TOOLS = new Set([ "background_task", ]); +// Remind again after this many direct tool calls without using agents +export const DIRECT_TOOL_CALLS_BEFORE_REMINDER = 5; + export const REMINDER_MESSAGE = ` [Agent Usage Reminder] @@ -51,3 +54,18 @@ WHY: ALWAYS prefer: Multiple parallel background_task calls > Direct tool calls `; + +export const IMPLEMENTATION_PHASE_REMINDER = ` +[Parallelism Reminder - Implementation Phase] + +You're doing direct tool calls. Consider delegating to background agents: + +\`\`\`typescript +// Instead of sequential work, PARALLELIZE: +background_task(agent="frontend-ui-ux-engineer", prompt="Build the UI...") +background_task(agent="document-writer", prompt="Write the docs...") +// Meanwhile YOU work on core logic +\`\`\` + +**Ask before each task**: Can a specialist agent handle this while I work on something else? +`; diff --git a/src/hooks/agent-usage-reminder/index.ts b/src/hooks/agent-usage-reminder/index.ts index bc7f3243f..e3d5ac633 100644 --- a/src/hooks/agent-usage-reminder/index.ts +++ b/src/hooks/agent-usage-reminder/index.ts @@ -4,7 +4,13 @@ import { saveAgentUsageState, clearAgentUsageState, } from "./storage"; -import { TARGET_TOOLS, AGENT_TOOLS, REMINDER_MESSAGE } from "./constants"; +import { + TARGET_TOOLS, + AGENT_TOOLS, + REMINDER_MESSAGE, + IMPLEMENTATION_PHASE_REMINDER, + DIRECT_TOOL_CALLS_BEFORE_REMINDER, +} from "./constants"; import type { AgentUsageState } from "./types"; interface ToolExecuteInput { @@ -37,6 +43,8 @@ export function createAgentUsageReminderHook(_ctx: PluginInput) { agentUsed: false, reminderCount: 0, updatedAt: Date.now(), + lastAgentUseAt: 0, + directToolCallsSinceAgent: 0, }; sessionStates.set(sessionID, state); } @@ -46,6 +54,8 @@ export function createAgentUsageReminderHook(_ctx: PluginInput) { function markAgentUsed(sessionID: string): void { const state = getOrCreateState(sessionID); state.agentUsed = true; + state.lastAgentUseAt = Date.now(); + state.directToolCallsSinceAgent = 0; state.updatedAt = Date.now(); saveAgentUsageState(state); } @@ -73,13 +83,26 @@ export function createAgentUsageReminderHook(_ctx: PluginInput) { const state = getOrCreateState(sessionID); - if (state.agentUsed) { + // First time: never used agents - show full reminder + if (!state.agentUsed) { + output.output += REMINDER_MESSAGE; + state.reminderCount++; + state.updatedAt = Date.now(); + saveAgentUsageState(state); return; } - output.output += REMINDER_MESSAGE; - state.reminderCount++; + // Has used agents before - track direct calls and remind periodically + state.directToolCallsSinceAgent++; state.updatedAt = Date.now(); + + // Remind again after N direct tool calls without using agents + if (state.directToolCallsSinceAgent >= DIRECT_TOOL_CALLS_BEFORE_REMINDER) { + output.output += IMPLEMENTATION_PHASE_REMINDER; + state.reminderCount++; + state.directToolCallsSinceAgent = 0; + } + saveAgentUsageState(state); }; diff --git a/src/hooks/agent-usage-reminder/types.ts b/src/hooks/agent-usage-reminder/types.ts index ffbd8f0c2..1087ecb5b 100644 --- a/src/hooks/agent-usage-reminder/types.ts +++ b/src/hooks/agent-usage-reminder/types.ts @@ -3,4 +3,6 @@ export interface AgentUsageState { agentUsed: boolean; reminderCount: number; updatedAt: number; + lastAgentUseAt: number; + directToolCallsSinceAgent: number; } diff --git a/src/hooks/keyword-detector/constants.ts b/src/hooks/keyword-detector/constants.ts index ff7d831fd..7702eacb9 100644 --- a/src/hooks/keyword-detector/constants.ts +++ b/src/hooks/keyword-detector/constants.ts @@ -14,7 +14,10 @@ export const KEYWORD_DETECTORS: Array<{ pattern: RegExp; message: string }> = [ 1. **BEFORE any action**: Create TODOs FIRST. Break down into atomic, granular steps. 2. **Be excessively detailed**: 10 small TODOs > 3 vague TODOs. Err on the side of too many. 3. **Real-time updates**: Mark \`in_progress\` before starting, \`completed\` IMMEDIATELY after. NEVER batch. -4. **One at a time**: Only ONE TODO should be \`in_progress\` at any moment. +4. **Strategic parallelism**: + - Mark ONE TODO \`in_progress\` for YOUR direct work + - DELEGATE other TODOs to background agents simultaneously + - Multiple agents work in parallel; you track your one active task 5. **Sub-tasks**: Complex TODO? Break it into sub-TODOs. Keep granularity high. 6. **Questions too**: User asks a question? TODO: "Answer with evidence: [question]" @@ -42,9 +45,32 @@ Check for test infrastructure FIRST. If exists, follow strictly: **NEVER write implementation before test. NEVER delete failing tests.** -## AGENT DEPLOYMENT +## AGENT DEPLOYMENT (THROUGHOUT ENTIRE SESSION) -Fire available agents in PARALLEL via background tasks. Use explore/librarian agents liberally (multiple concurrent if needed). +**Parallelism applies to ALL phases - exploration AND implementation!** + +Fire agents in PARALLEL via background tasks: +- **Exploration**: explore/librarian for research (multiple concurrent) +- **Implementation**: frontend-ui-ux-engineer, document-writer for delegatable work +- **Verification**: explore agents to check patterns across files + +**Before EACH TODO, ask**: "Can a specialist agent handle this while I work on something else?" + +\`\`\`typescript +// EXPLORATION PHASE - parallel research +background_task(agent="explore", prompt="Find all auth implementations...") +background_task(agent="librarian", prompt="Find JWT best practices...") + +// IMPLEMENTATION PHASE - parallel delegation +background_task(agent="frontend-ui-ux-engineer", prompt="Build the UI...") +background_task(agent="document-writer", prompt="Write the docs...") +// Meanwhile YOU work on backend/core logic + +// Collect when needed +background_output(task_id="...") +\`\`\` + +**NEVER fall into sequential-only mode during implementation!** ## EVIDENCE-BASED ANSWERS diff --git a/src/hooks/rules-injector/finder.ts b/src/hooks/rules-injector/finder.ts index 3bf293946..e5faf2d45 100644 --- a/src/hooks/rules-injector/finder.ts +++ b/src/hooks/rules-injector/finder.ts @@ -15,15 +15,23 @@ import { } from "./constants"; import type { RuleFileCandidate } from "./types"; +function normalizePathForMatching(p: string): string { + return p.replace(/\\/g, "/") +} + function isGitHubInstructionsDir(dir: string): boolean { - return dir.includes(".github/instructions") || dir.endsWith(".github/instructions"); + const normalized = normalizePathForMatching(dir) + return ( + normalized.includes(".github/instructions") || + normalized.endsWith(".github/instructions") + ) } function isValidRuleFile(fileName: string, dir: string): boolean { if (isGitHubInstructionsDir(dir)) { - return GITHUB_INSTRUCTIONS_PATTERN.test(fileName); + return GITHUB_INSTRUCTIONS_PATTERN.test(fileName) } - return RULE_EXTENSIONS.some((ext) => fileName.endsWith(ext)); + return RULE_EXTENSIONS.some((ext) => fileName.endsWith(ext)) } /** @@ -190,11 +198,11 @@ export function findRuleFiles( seenRealPaths.add(realPath); candidates.push({ - path: filePath, + path: normalizePathForMatching(filePath), realPath, isGlobal: false, distance, - }); + }) } } @@ -218,12 +226,12 @@ export function findRuleFiles( if (!seenRealPaths.has(realPath)) { seenRealPaths.add(realPath); candidates.push({ - path: filePath, + path: normalizePathForMatching(filePath), realPath, isGlobal: false, distance: 0, isSingleFile: true, - }); + }) } } } catch { @@ -244,11 +252,11 @@ export function findRuleFiles( seenRealPaths.add(realPath); candidates.push({ - path: filePath, + path: normalizePathForMatching(filePath), realPath, isGlobal: true, distance: 9999, // Global rules always have max distance - }); + }) } // Sort by distance (closest first, then global rules last) diff --git a/src/hooks/todo-continuation-enforcer.ts b/src/hooks/todo-continuation-enforcer.ts index 5e16354d7..27e5915af 100644 --- a/src/hooks/todo-continuation-enforcer.ts +++ b/src/hooks/todo-continuation-enforcer.ts @@ -37,10 +37,19 @@ interface SessionState { const CONTINUATION_PROMPT = `[SYSTEM REMINDER - TODO CONTINUATION] -Incomplete tasks remain in your todo list. Continue working on the next pending task. +Incomplete tasks remain. Review and PARALLELIZE where possible: +1. **Scan remaining TODOs** - Which can be delegated to specialists? +2. **Delegate in parallel**: + - UI work → \`background_task(agent="frontend-ui-ux-engineer", ...)\` + - Documentation → \`background_task(agent="document-writer", ...)\` + - Research → \`background_task(agent="explore" or "librarian", ...)\` +3. **Work on YOUR task** - Handle what only you can do +4. **Collect results** - \`background_output\` before marking complete + +- Fire multiple background agents simultaneously - Proceed without asking for permission -- Mark each task complete when finished +- Mark each task complete when verified - Do not stop until all tasks are done` const COUNTDOWN_SECONDS = 2 diff --git a/src/shared/opencode-config-dir.ts b/src/shared/opencode-config-dir.ts index 3a11ee93e..2e2c5feda 100644 --- a/src/shared/opencode-config-dir.ts +++ b/src/shared/opencode-config-dir.ts @@ -1,6 +1,6 @@ import { existsSync } from "node:fs" import { homedir } from "node:os" -import { join } from "node:path" +import { join, posix } from "node:path" export type OpenCodeBinaryType = "opencode" | "opencode-desktop" @@ -67,6 +67,12 @@ function getCliConfigDir(): string { } const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config") + + // If user provided a POSIX-style XDG path (common in tests/CI), preserve it even on Windows. + if (process.env.XDG_CONFIG_HOME && xdgConfig.startsWith("/") && !xdgConfig.includes("\\")) { + return posix.join(xdgConfig, "opencode") + } + return join(xdgConfig, "opencode") }