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
188 changes: 188 additions & 0 deletions .claudeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# DevFlow .claudeignore - Protects against sensitive files and context pollution
# Generated by DevFlow - Edit as needed for your project

# === SECURITY: Sensitive Files ===
# Environment and secrets
.env
.env.*
.env.local
.env.*.local
*.env
.envrc

# Credentials and keys
*.key
*.pem
*.p12
*.pfx
*.cer
*.crt
*.der
id_rsa
id_dsa
id_ecdsa
id_ed25519
*.ppk
*_rsa
*_dsa
*secret*
*password*
*credential*
credentials.json
secrets.json
secrets.yaml
secrets.yml

# Cloud provider credentials
.aws/credentials
.aws/config
.gcp/credentials.json
.azure/credentials

# Package manager credentials
.npmrc
.pypirc
.gem/credentials
pip.conf

# Database
*.sql
*.db
*.sqlite
*.sqlite3

# === DEPENDENCIES & BUILD ===
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.pnpm-store/

# Python
__pycache__/
*.py[cod]
*$py.class
.Python
env/
venv/
ENV/
.venv/
pip-log.txt
pip-delete-this-directory.txt
.eggs/
*.egg-info/
dist/
build/
*.whl

# Ruby
vendor/bundle/
.bundle/

# Go
vendor/
go.sum

# Rust
target/
Cargo.lock

# Java
target/
*.class
*.jar
*.war

# PHP
vendor/
composer.lock

# === BUILD ARTIFACTS ===
dist/
build/
out/
.next/
.nuxt/
.output/
.vite/
.cache/
.parcel-cache/
.turbo/
*.tsbuildinfo

# === LOGS & TEMP FILES ===
logs/
*.log
*.tmp
*.temp
*.swp
*.swo
*~
.DS_Store
Thumbs.db
*.bak
*.orig
*.rej
.cache

# === VERSION CONTROL ===
.git/
.svn/
.hg/
.gitignore

# === IDE & EDITORS ===
.vscode/
.idea/
*.sublime-*
*.code-workspace
.project
.classpath
.settings/

# === TEST COVERAGE ===
coverage/
.nyc_output/
htmlcov/
.coverage
.pytest_cache/
.tox/

# === OS-SPECIFIC ===
.DS_Store
.AppleDouble
.LSOverride
Thumbs.db
ehthumbs.db
Desktop.ini

# === MEDIA & LARGE FILES ===
*.mp4
*.avi
*.mov
*.wmv
*.flv
*.mp3
*.wav
*.zip
*.tar.gz
*.rar
*.7z
*.dmg
*.iso

# === DOCUMENTATION BUILD ===
site/
_site/
.docusaurus/
.vuepress/dist/

# === LOCK FILES (usually not needed for AI context) ===
package-lock.json
yarn.lock
pnpm-lock.yaml
Gemfile.lock
poetry.lock
Pipfile.lock
43 changes: 25 additions & 18 deletions src/cli/commands/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { exitOnError, exitOnNull, withReadOnlyContext, withServices } from '../s
import * as ui from '../ui.js';

/**
* Parsed arguments from CLI loop create command
* Parsed arguments from CLI loop create command.
* Discriminated union on `isPipeline`: pipeline variant has `pipelineSteps`,
* non-pipeline variant has `prompt`. Matches schedule parser pattern.
*/
interface ParsedLoopArgs {
readonly prompt?: string;
interface ParsedLoopBaseArgs {
readonly strategy: LoopStrategy;
readonly exitCondition: string;
readonly evalDirection?: 'minimize' | 'maximize';
Expand All @@ -20,11 +21,14 @@ interface ParsedLoopArgs {
readonly maxConsecutiveFailures?: number;
readonly cooldownMs?: number;
readonly freshContext: boolean;
readonly pipelineSteps?: readonly string[];
readonly priority?: 'P0' | 'P1' | 'P2';
readonly agent?: AgentProvider;
}

type ParsedLoopArgs =
| (ParsedLoopBaseArgs & { readonly isPipeline: true; readonly pipelineSteps: readonly string[] })
| (ParsedLoopBaseArgs & { readonly isPipeline: false; readonly prompt: string });

/**
* Parse and validate loop create arguments
* ARCHITECTURE: Pure function — no side effects, returns Result for testability
Expand Down Expand Up @@ -62,25 +66,25 @@ export function parseLoopCreateArgs(loopArgs: string[]): Result<ParsedLoopArgs,
direction = next;
i++;
} else if (arg === '--max-iterations' && next) {
maxIterations = parseInt(next);
maxIterations = parseInt(next, 10);
if (isNaN(maxIterations) || maxIterations < 0) {
return err('--max-iterations must be >= 0 (0 = unlimited)');
}
i++;
} else if (arg === '--max-failures' && next) {
maxFailures = parseInt(next);
maxFailures = parseInt(next, 10);
if (isNaN(maxFailures) || maxFailures < 0) {
return err('--max-failures must be >= 0');
}
i++;
} else if (arg === '--cooldown' && next) {
cooldown = parseInt(next);
cooldown = parseInt(next, 10);
if (isNaN(cooldown) || cooldown < 0) {
return err('--cooldown must be >= 0 (ms)');
}
i++;
} else if (arg === '--eval-timeout' && next) {
evalTimeout = parseInt(next);
evalTimeout = parseInt(next, 10);
if (isNaN(evalTimeout) || evalTimeout < 1000) {
return err('--eval-timeout must be >= 1000 (ms)');
}
Expand All @@ -105,8 +109,8 @@ export function parseLoopCreateArgs(loopArgs: string[]): Result<ParsedLoopArgs,
}
workingDirectory = pathResult.value;
i++;
} else if ((arg === '--agent' || arg === '-a') && next) {
if (next.startsWith('-')) {
} else if (arg === '--agent' || arg === '-a') {
if (!next || next.startsWith('-')) {
return err(`--agent requires an agent name (${AGENT_PROVIDERS.join(', ')})`);
}
if (!isAgentProvider(next)) {
Expand Down Expand Up @@ -159,8 +163,7 @@ export function parseLoopCreateArgs(loopArgs: string[]): Result<ParsedLoopArgs,
return err('Usage: beat loop <prompt> --until <cmd> [options]');
}

return ok({
prompt: isPipeline ? undefined : prompt,
const shared = {
strategy: isOptimize ? LoopStrategy.OPTIMIZE : LoopStrategy.RETRY,
exitCondition,
evalDirection: direction,
Expand All @@ -170,10 +173,14 @@ export function parseLoopCreateArgs(loopArgs: string[]): Result<ParsedLoopArgs,
maxConsecutiveFailures: maxFailures,
cooldownMs: cooldown,
freshContext: !continueContext,
pipelineSteps: isPipeline ? pipelineSteps : undefined,
priority,
agent,
});
};

if (isPipeline) {
return ok({ ...shared, isPipeline: true as const, pipelineSteps });
}
return ok({ ...shared, isPipeline: false as const, prompt });
}

export async function handleLoopCommand(subCmd: string | undefined, loopArgs: string[]): Promise<void> {
Expand Down Expand Up @@ -216,7 +223,7 @@ async function handleLoopCreate(loopArgs: string[]): Promise<void> {
const { loopService } = await withServices(s);

const result = await loopService.createLoop({
prompt: args.prompt,
prompt: args.isPipeline ? undefined : args.prompt,
strategy: args.strategy,
exitCondition: args.exitCondition,
evalDirection: toOptimizeDirection(args.evalDirection),
Expand All @@ -226,7 +233,7 @@ async function handleLoopCreate(loopArgs: string[]): Promise<void> {
maxConsecutiveFailures: args.maxConsecutiveFailures,
cooldownMs: args.cooldownMs,
freshContext: args.freshContext,
pipelineSteps: args.pipelineSteps,
pipelineSteps: args.isPipeline ? args.pipelineSteps : undefined,
priority: args.priority ? Priority[args.priority] : undefined,
agent: args.agent,
});
Expand Down Expand Up @@ -264,7 +271,7 @@ async function handleLoopList(loopArgs: string[]): Promise<void> {
status = next;
i++;
} else if (arg === '--limit' && next) {
limit = parseInt(next);
limit = parseInt(next, 10);
i++;
}
}
Expand Down Expand Up @@ -324,7 +331,7 @@ async function handleLoopGet(loopArgs: string[]): Promise<void> {
let historyLimit: number | undefined;
const hlIdx = loopArgs.indexOf('--history-limit');
if (hlIdx !== -1 && loopArgs[hlIdx + 1]) {
historyLimit = parseInt(loopArgs[hlIdx + 1]);
historyLimit = parseInt(loopArgs[hlIdx + 1], 10);
}

const s = ui.createSpinner();
Expand Down
Loading
Loading