Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion src/agents/sisyphus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
13 changes: 8 additions & 5 deletions src/features/opencode-skill-loader/async-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export async function loadSkillFromPathAsync(
const content = await readFile(skillPath, "utf-8")
const { data, body, parseError } = parseFrontmatter<SkillMetadata>(content)
if (parseError) return null

const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content)
const mcpJsonMcp = await loadMcpJsonFromDirAsync(resolvedPath)
const mcpConfig = mcpJsonMcp || frontmatterMcp
Expand Down Expand Up @@ -133,7 +133,10 @@ function parseAllowedTools(allowedTools: string | undefined): string[] | undefin
return allowedTools.split(/\s+/).filter(Boolean)
}

export async function discoverSkillsInDirAsync(skillsDir: string): Promise<LoadedSkill[]> {
export async function discoverSkillsInDirAsync(
skillsDir: string,
scope: SkillScope = "opencode-project"
): Promise<LoadedSkill[]> {
try {
const entries = await readdir(skillsDir, { withFileTypes: true })

Expand All @@ -149,12 +152,12 @@ export async function discoverSkillsInDirAsync(skillsDir: string): Promise<Loade
const skillMdPath = join(resolvedPath, "SKILL.md")
try {
await readFile(skillMdPath, "utf-8")
return await loadSkillFromPathAsync(skillMdPath, resolvedPath, dirName, "opencode-project")
return await loadSkillFromPathAsync(skillMdPath, resolvedPath, dirName, scope)
} catch {
const namedSkillMdPath = join(resolvedPath, `${dirName}.md`)
try {
await readFile(namedSkillMdPath, "utf-8")
return await loadSkillFromPathAsync(namedSkillMdPath, resolvedPath, dirName, "opencode-project")
return await loadSkillFromPathAsync(namedSkillMdPath, resolvedPath, dirName, scope)
} catch {
return null
}
Expand All @@ -163,7 +166,7 @@ export async function discoverSkillsInDirAsync(skillsDir: string): Promise<Loade

if (isMarkdownFile(entry)) {
const skillName = basename(entry.name, ".md")
return await loadSkillFromPathAsync(entryPath, skillsDir, skillName, "opencode-project")
return await loadSkillFromPathAsync(entryPath, skillsDir, skillName, scope)
}

return null
Expand Down
7 changes: 4 additions & 3 deletions src/features/opencode-skill-loader/discover-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ parentPort.once("message", (data: { port: MessagePort }) => {

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))
)
Comment on lines +34 to +37
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 4, 2026

Choose a reason for hiding this comment

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

P1: Using only scopes[0] applies the same scope to all directories, ignoring the parallel array relationship. Each directory should use its corresponding scope from the array.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/opencode-skill-loader/discover-worker.ts, line 34:

<comment>Using only `scopes[0]` applies the same scope to all directories, ignoring the parallel array relationship. Each directory should use its corresponding scope from the array.</comment>

<file context>
@@ -31,9 +31,10 @@ parentPort.once(&quot;message&quot;, (data: { port: MessagePort }) =&gt; {
-      const results = await Promise.all(
-        input.dirs.map(dir =&gt; discoverSkillsInDirAsync(dir))
-      )
+        const scope = input.scopes[0] ?? &quot;opencode-project&quot;
+        const results = await Promise.all(
+          input.dirs.map((dir) =&gt; discoverSkillsInDirAsync(dir, scope))
</file context>
Suggested change
const scope = input.scopes[0] ?? "opencode-project"
const results = await Promise.all(
input.dirs.map((dir) => discoverSkillsInDirAsync(dir, scope))
)
const results = await Promise.all(
input.dirs.map((dir, index) => discoverSkillsInDirAsync(dir, input.scopes[index] ?? "opencode-project"))
)
Fix with Cubic


const skills = results.flat()

Expand Down
5 changes: 2 additions & 3 deletions src/features/opencode-skill-loader/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
26 changes: 20 additions & 6 deletions src/features/opencode-skill-loader/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ function loadSkillFromPath(
): LoadedSkill | null {
try {
const content = readFileSync(skillPath, "utf-8")
const { data } = parseFrontmatter<SkillMetadata>(content)
const { data, parseError } = parseFrontmatter<SkillMetadata>(content)
if (parseError) return null
const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content)
const mcpJsonMcp = loadMcpJsonFromDir(resolvedPath)
const mcpConfig = mcpJsonMcp || frontmatterMcp
Expand All @@ -84,8 +85,12 @@ function loadSkillFromPath(
load: async () => {
if (!lazyContent.loaded) {
const fileContent = await fs.readFile(skillPath, "utf-8")
const { body } = parseFrontmatter<SkillMetadata>(fileContent)
lazyContent.content = `<skill-instruction>
const { body, parseError } = parseFrontmatter<SkillMetadata>(fileContent)

if (parseError) {
lazyContent.content = ""
} else {
lazyContent.content = `<skill-instruction>
Base directory for this skill: ${resolvedPath}/
File references (@path) in this skill are relative to this directory.

Expand All @@ -95,6 +100,8 @@ ${body.trim()}
<user-request>
$ARGUMENTS
</user-request>`
}

lazyContent.loaded = true
}
return lazyContent.content!
Expand Down Expand Up @@ -137,7 +144,8 @@ async function loadSkillFromPathAsync(
): Promise<LoadedSkill | null> {
try {
const content = await fs.readFile(skillPath, "utf-8")
const { data } = parseFrontmatter<SkillMetadata>(content)
const { data, parseError } = parseFrontmatter<SkillMetadata>(content)
if (parseError) return null
const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content)
const mcpJsonMcp = loadMcpJsonFromDir(resolvedPath)
const mcpConfig = mcpJsonMcp || frontmatterMcp
Expand All @@ -153,8 +161,12 @@ async function loadSkillFromPathAsync(
load: async () => {
if (!lazyContent.loaded) {
const fileContent = await fs.readFile(skillPath, "utf-8")
const { body } = parseFrontmatter<SkillMetadata>(fileContent)
lazyContent.content = `<skill-instruction>
const { body, parseError } = parseFrontmatter<SkillMetadata>(fileContent)

if (parseError) {
lazyContent.content = ""
} else {
lazyContent.content = `<skill-instruction>
Base directory for this skill: ${resolvedPath}/
File references (@path) in this skill are relative to this directory.

Expand All @@ -164,6 +176,8 @@ ${body.trim()}
<user-request>
$ARGUMENTS
</user-request>`
}

lazyContent.loaded = true
}
return lazyContent.content!
Expand Down
2 changes: 1 addition & 1 deletion src/features/skill-mcp-manager/env-cleaner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function createCleanMcpEnvironment(
const cleanEnv: Record<string, string> = {}

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) {
Expand Down
18 changes: 18 additions & 0 deletions src/hooks/agent-usage-reminder/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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?
`;
31 changes: 27 additions & 4 deletions src/hooks/agent-usage-reminder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -37,6 +43,8 @@ export function createAgentUsageReminderHook(_ctx: PluginInput) {
agentUsed: false,
reminderCount: 0,
updatedAt: Date.now(),
lastAgentUseAt: 0,
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 4, 2026

Choose a reason for hiding this comment

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

P1: Missing backward compatibility for new state fields. When loading persisted state created before this PR, lastAgentUseAt and directToolCallsSinceAgent will be undefined. This causes undefined++ to produce NaN, and the periodic reminder condition NaN >= N will always be false, breaking the feature for existing users. Provide defaults for the new fields when loading persisted state.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/hooks/agent-usage-reminder/index.ts, line 46:

<comment>Missing backward compatibility for new state fields. When loading persisted state created before this PR, `lastAgentUseAt` and `directToolCallsSinceAgent` will be `undefined`. This causes `undefined++` to produce `NaN`, and the periodic reminder condition `NaN &gt;= N` will always be `false`, breaking the feature for existing users. Provide defaults for the new fields when loading persisted state.</comment>

<file context>
@@ -37,6 +43,8 @@ export function createAgentUsageReminderHook(_ctx: PluginInput) {
         agentUsed: false,
         reminderCount: 0,
         updatedAt: Date.now(),
+        lastAgentUseAt: 0,
+        directToolCallsSinceAgent: 0,
       };
</file context>
Fix with Cubic

directToolCallsSinceAgent: 0,
};
sessionStates.set(sessionID, state);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
};

Expand Down
2 changes: 2 additions & 0 deletions src/hooks/agent-usage-reminder/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export interface AgentUsageState {
agentUsed: boolean;
reminderCount: number;
updatedAt: number;
lastAgentUseAt: number;
directToolCallsSinceAgent: number;
}
32 changes: 29 additions & 3 deletions src/hooks/keyword-detector/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading