A self-compiling tool to process vibe-coded projects using prompt stacks and LLM generation.
vibec transforms stacks of prompts into code and tests, supporting static .md and dynamic .js plugins. It outputs staged artifacts (output/stages/) for Git history and a current runtime version (output/current/) aggregated from all stages with a "Last-Wins" strategy. It can compile itself using its own structure.
stacks/: Prompt stacks (e.g.,core/,generation/,tests/).- Numbered prompts:
001_cli.md(processed sequentially). - Multi-file output syntax:
# CLI Parser Generate a CLI parser for a Node.js tool. ## Output: core/cli.js ## Output: core/cli_utils.js
plugins/: Included in every LLM request for the stack..md: Static text (e.g., "Use ES6 syntax")..js: Dynamic async plugins (e.g.,async (context) => { ... }).
- Numbered prompts:
output/: Generated files.stages/: Numbered dirs (e.g.,001/core/cli.js,001/core/cli_utils.js).current/: Latest files (e.g.,core/cli.js), merged with "Last-Wins" (later stages overwrite earlier ones).
.vibec_hashes.json: Tracks prompt hashes and test results.bootstrap.js: Runs self-compilation.vibec.json: Optional config.bin/vibec-prebuilt.js: Prebuilt minimalvibec.
vibec processes prompts in a specific order:
- Numerical Order First: All prompts with the same numerical prefix (e.g.,
001_*.md) are processed before moving to the next number (002_*.md). - Stack Order Second: Within each numerical stage, stacks are processed in the order specified (e.g., if
--stacks core,generation,tests, thencore/001_*.md→generation/001_*.md→tests/001_*.md).
This ordering ensures that:
- Each numerical stage represents a logical progression in your codebase
- Dependencies between stacks within the same stage are respected
- Later numerical stages can build upon earlier ones
flowchart TD
subgraph "Stage_001"
S1C[core] --> S1G[generation]
S1G --> S1T[tests]
end
subgraph "Stage_002"
S2C[core] --> S2G[generation]
S2G --> S2T[tests]
end
Stage_001 --> Stage_002
After all stages are processed, generated files are merged into output/current/ using a "Last-Wins" strategy:
- Files from later numerical stages overwrite earlier ones
- Within the same numerical stage, files from later stacks in the processing order overwrite earlier ones
vibec --stacks core,generation,tests --test-cmd "npm test" --retries 2 --plugin-timeout 5000 --no-overwrite--stacks: Comma-separated stack list.--test-cmd: Test command.--retries: Retry failed tests (default: 0).--plugin-timeout: Max ms for.jsplugins (default: 10000).--no-overwrite: Fail ifoutput/current/files would be overwritten.
vibec.json:
{
"stacks": ["core", "generation", "tests"],
"testCmd": "npm test",
"retries": 2,
"pluginTimeout": 5000,
"pluginParams": {
"dump_files": { "files": ["src/main.js", "README.md"] }
}
}Env vars: VIBEC_STACKS, VIBEC_TEST_CMD, etc.
Run node bootstrap.js to compile vibec from stacks/. It:
- Checks
output/current/core/vibec.js. - Falls back to
bin/vibec-prebuilt.js. - Runs
vibec --stacks core,generation,tests --test-cmd "npm test" --retries 2.
.js plugins receive:
{
config: { /* vibec.json, including pluginParams */ },
stack: "core",
promptNumber: 1,
promptContent: "# CLI Parser\nGenerate a ... \n## Context: src/main.js\n## Output: core/cli.js",
workingDir: "/path/to/output/current",
testCmd: "npm test",
testResult: { errorCode: 1, stdout: "...", stderr: "..." }
}Each prompt goes through the following lifecycle:
- Loading: Prompt content is loaded from the
.mdfile - Plugin Integration:
- Static plugins (
.md) are appended to the prompt - Dynamic plugins (
.js) are executed and their output is integrated
- Static plugins (
- LLM Generation: The combined prompt is sent to an LLM service
- Output Parsing: Generated content is parsed and written to files
- Testing: The test command is run to validate the output
- Retry Loop (optional): If tests fail and retries > 0, the process repeats with test results
flowchart LR
A[Load Prompt] --> B[Apply Plugins]
B --> C[LLM Generate]
C --> D[Write Files]
D --> E[Run Tests]
E -->|Success| F[Mark as Success]
E -->|Failure| G{Retries?}
G -->|Yes| C
G -->|No| H[Update Hash File]
F --> I[Process Next Stack]
H --> I
- Edit
stacks/core/001_cli.md:# CLI Parser Generate a command-line interface parser for a Node.js tool. It should handle flags like `--help` and `--version`, and support subcommands. Use existing generated code as context. ## Context: src/main.js, README.md ## Output: core/cli.js ## Output: core/cli_utils.js
- Run
node bootstrap.js. - Use
output/current/core/vibec.js.
- Clone repo.
- Ensure
bin/vibec-prebuilt.jsexists. - Run
node bootstrap.js.
For iterative development:
- Edit prompts in a specific numerical stage
- Run
node bootstrap.js - Check
output/stages/NNN/for immediate results - Look at
output/current/for the final merged result
Tests validate each stage's output. Consider:
- Unit tests for individual components
- Integration tests for the complete stage
- End-to-end tests that validate the final
output/current/
When creating .js plugins:
- Keep them focused on a single responsibility
- Handle errors gracefully
- Return structured content when possible
- Use async/await for all asynchronous operations
- Add debug logging to aid troubleshooting
dump_files.js:const fs = require("fs").promises; const path = require("path"); module.exports = async (context) => { const output = []; // External files from ## Context: const contextMatch = context.promptContent.match(/## Context: (.+)/); const externalFiles = contextMatch ? contextMatch[1].split(",").map(f => f.trim()) : context.config.pluginParams.dump_files?.files || []; if (externalFiles.length) { const contents = await Promise.all( externalFiles.map(async (file) => { try { const content = await fs.readFile(file, "utf8"); return "```javascript " + file + "\n" + content + "\n```"; } catch (e) { return "```javascript " + file + "\n// File not found\n```"; } }) ); output.push(...contents); } // Aggregate files from output/current/<stack>/ const stackDir = path.join(context.workingDir, context.stack); let generatedFiles = []; try { generatedFiles = await fs.readdir(stackDir); } catch (e) { // Dir might not exist yet } if (generatedFiles.length) { const contents = await Promise.all( generatedFiles.map(async (file) => { const fullPath = path.join(stackDir, file); const content = await fs.readFile(fullPath, "utf8"); return "```javascript " + path.join(context.stack, file) + "\n" + content + "\n```"; }) ); output.push("Generated files in current stack:\n", ...contents); } return output.length ? output.join("\n") : "No context files available."; };
graph TD
subgraph ProjectStructure["Project Structure"]
A[stacks/] --> A1[core/]
A[stacks/] --> A2[generation/]
A[stacks/] --> A3[tests/]
A1 --> A1_1[001_cli.md]
A1 --> A1_2[002_parser.md]
A2 --> A2_1[001_generator.md]
A3 --> A3_1[001_cli_tests.md]
P[plugins/] --> P1[static.md]
P[plugins/] --> P2[dynamic.js]
O[output/] --> O1[stages/]
O[output/] --> O2[current/]
O1 --> O1_1[001/]
O1 --> O1_2[002/]
O1_1 --> O1_1_1[core/]
O1_1 --> O1_1_2[generation/]
O1_1 --> O1_1_3[tests/]
C[.vibec_hashes.json]
B[bootstrap.js]
V[vibec.json]
BIN[bin/vibec-prebuilt.js]
end
flowchart TD
Start([Start]) --> Bootstrap[Run bootstrap.js]
Bootstrap --> CheckCurrent{Check output/current exists?}
CheckCurrent -->|Yes| RunVibec[Run vibec from output/current]
CheckCurrent -->|No| UsePrebuilt[Use bin/vibec-prebuilt.js]
UsePrebuilt --> RunVibec
RunVibec --> ProcessStages[Process stages numerically]
subgraph NumericalStageProcessing["Numerical Stage Processing"]
ProcessStages --> Stage1[Process Stage 001]
Stage1 --> Stage2[Process Stage 002]
Stage2 --> StageN[Process Stage N...]
Stage1 --> ProcessStacks[Process specified stacks]
subgraph StackProcessing["Stack Processing within Stage"]
ProcessStacks --> Stack1[Process core/]
Stack1 --> Stack2[Process generation/]
Stack2 --> Stack3[Process tests/]
Stack1 --> GenFiles[Generate output files]
GenFiles --> RunTests[Run test command]
RunTests --> CheckTests{Tests Pass?}
CheckTests -->|Yes| NextStack[Move to next stack]
CheckTests -->|No, retries left| Retry[Retry]
Retry --> GenFiles
CheckTests -->|No, no retries| FailStage[Fail]
end
end
StageN --> MergeOutput[Merge all stages to output/current/]
MergeOutput --> End([End])
- Plugin timeouts: Increase
--plugin-timeoutfor complex plugins - Test failures: Use
--retriesto give the LLM more chances with test output - Overwritten files: Use
--no-overwriteto prevent accidental overwrites
- Check
.vibec_hashes.jsonfor test result history - Examine
output/stages/to see intermediate results - Look at test output for specific error messages
- Try running with a smaller subset of stacks for focused debugging