Skip to content

Commit 079e385

Browse files
committed
feat: felt setup claude/codex installs felt/constitution/ralph skills
1 parent 16557ed commit 079e385

File tree

9 files changed

+999
-0
lines changed

9 files changed

+999
-0
lines changed

cmd/setup.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package cmd
22

33
import (
4+
"embed"
45
"encoding/json"
56
"fmt"
7+
"io/fs"
68
"os"
79
"path/filepath"
810
"strings"
911

1012
"github.com/spf13/cobra"
1113
)
1214

15+
//go:embed skills
16+
var embeddedSkills embed.FS
17+
1318
var setupCmd = &cobra.Command{
1419
Use: "setup",
1520
Short: "Setup integrations",
@@ -34,6 +39,11 @@ Use --uninstall to remove the hooks.`,
3439
return err
3540
}
3641
fmt.Println()
42+
skillsTarget := filepath.Join(os.Getenv("HOME"), ".claude", "skills")
43+
if err := installSkills(skillsTarget); err != nil {
44+
fmt.Printf("warning: could not install skills: %v\n", err)
45+
}
46+
fmt.Println()
3747
fmt.Println("You may want to put something like the following in your CLAUDE.md, adjusted to match your work style:")
3848
fmt.Println()
3949
fmt.Println(claudeMDSnippet())
@@ -59,6 +69,11 @@ Use --uninstall to remove it.`,
5969
return err
6070
}
6171
fmt.Println()
72+
skillsTarget := filepath.Join(os.Getenv("HOME"), ".agents", "skills")
73+
if err := installSkills(skillsTarget); err != nil {
74+
fmt.Printf("warning: could not install skills: %v\n", err)
75+
}
76+
fmt.Println()
6277
fmt.Println("You may want to put something like the following in your AGENTS.md, adjusted to match your work style:")
6378
fmt.Println()
6479
fmt.Println(claudeMDSnippet())
@@ -94,6 +109,99 @@ func claudeMDSnippet() string {
94109
"Follow the data: curious, not confirmatory.\n"
95110
}
96111

112+
// skillsDir returns the path where felt extracts its bundled skills.
113+
func skillsDir() (string, error) {
114+
home, err := os.UserHomeDir()
115+
if err != nil {
116+
return "", err
117+
}
118+
return filepath.Join(home, ".local", "share", "felt", "skills"), nil
119+
}
120+
121+
// installSkills extracts bundled skills to ~/.local/share/felt/skills and
122+
// symlinks each skill directory into targetDir (e.g. ~/.claude/skills).
123+
func installSkills(targetDir string) error {
124+
src, err := skillsDir()
125+
if err != nil {
126+
return err
127+
}
128+
129+
// Extract embedded skills
130+
if err := extractEmbeddedSkills(src); err != nil {
131+
return fmt.Errorf("extracting skills: %w", err)
132+
}
133+
134+
// Ensure target directory exists
135+
if err := os.MkdirAll(targetDir, 0755); err != nil {
136+
return fmt.Errorf("creating skills directory: %w", err)
137+
}
138+
139+
// Symlink each skill directory
140+
entries, err := os.ReadDir(src)
141+
if err != nil {
142+
return err
143+
}
144+
145+
for _, e := range entries {
146+
if !e.IsDir() {
147+
continue
148+
}
149+
linkPath := filepath.Join(targetDir, e.Name())
150+
target := filepath.Join(src, e.Name())
151+
152+
if existing, err := os.Lstat(linkPath); err == nil {
153+
if existing.Mode()&os.ModeSymlink != 0 {
154+
fmt.Printf("· %s already linked\n", e.Name())
155+
continue
156+
}
157+
fmt.Printf("· %s exists (not a symlink, skipping)\n", e.Name())
158+
continue
159+
}
160+
161+
if err := os.Symlink(target, linkPath); err != nil {
162+
return fmt.Errorf("symlinking %s: %w", e.Name(), err)
163+
}
164+
fmt.Printf("✓ Linked skill: %s\n", e.Name())
165+
}
166+
return nil
167+
}
168+
169+
// extractEmbeddedSkills writes the embedded skills/ tree to dest.
170+
func extractEmbeddedSkills(dest string) error {
171+
return fs.WalkDir(embeddedSkills, "skills", func(path string, d fs.DirEntry, err error) error {
172+
if err != nil {
173+
return err
174+
}
175+
// path is like "skills/felt/SKILL.md" — strip leading "skills/"
176+
rel, err := filepath.Rel("skills", path)
177+
if err != nil {
178+
return err
179+
}
180+
target := filepath.Join(dest, rel)
181+
182+
if d.IsDir() {
183+
return os.MkdirAll(target, 0755)
184+
}
185+
186+
data, err := embeddedSkills.ReadFile(path)
187+
if err != nil {
188+
return err
189+
}
190+
191+
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
192+
return err
193+
}
194+
195+
// Preserve executable bit for scripts
196+
mode := fs.FileMode(0644)
197+
if strings.Contains(path, "/scripts/") {
198+
mode = 0755
199+
}
200+
201+
return os.WriteFile(target, data, mode)
202+
})
203+
}
204+
97205
// rcFilePath returns the shell RC file path based on $SHELL.
98206
func rcFilePath() (string, error) {
99207
home, err := os.UserHomeDir()

cmd/skills/constitution/SKILL.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
---
2+
name: constitution
3+
description: >
4+
Draft a ralph constitution — a fiber spec describing a desired state for
5+
autonomous iteration. Study the problem space, shape the spec interactively,
6+
then launch the ralph loop. Use for any work where adaptation matters more
7+
than a fixed plan: science, refactoring, exploration, creative work.
8+
Triggers: "constitution", "constitute", "ralph spec", "set up a ralph",
9+
"create a ralph", "ralph fiber".
10+
---
11+
12+
# Constitution
13+
14+
A constitution is a design document with trust built in. Like a governmental constitution, it lays out principles and aspirations — not specific laws, not the current state of affairs. It's designed to outlast any single agent or iteration and remain valid as the world changes around it. A good constitution never says "50 files remain" because that's a snapshot that goes stale; it says "check `grep -r 'old_pattern'`" because that's a principle that stays true until the work is done.
15+
16+
Constitutions don't prescribe steps. They describe what the system looks like when it's right — the desired state, in both senses of the word. Nothing in the constitution should become confusing or unnecessary as the desired state is reached. Whoever works from it surveys reality, reasons about the gap, and decides what's highest value. In a ralph loop, each iteration does this with fresh context.
17+
18+
This matters most in science and exploratory work, where each decision is informed by the result just before it. A plan assumes you know the path; a constitution trusts the agent to find it — with taste, judgment, and fresh eyes each time.
19+
20+
**Separation of context: if you craft, you never do the work yourself.**
21+
22+
## Workflow
23+
24+
1. **Study** — Read relevant files, understand existing patterns. This informs the *constitution*, not implementation. The goal is pointers that iterations will follow.
25+
26+
2. **Draft** — Create the fiber with status `open`:
27+
```bash
28+
felt add "Constitution title" -s open
29+
```
30+
Then edit the body in `.felt/<id>.md`. Fill in what you can — don't wait until it's perfect.
31+
32+
3. **Refine** — Show the draft, get feedback, revise. Use AskUserQuestion for structured choices. Repeat until it feels solid.
33+
34+
4. **Launch** — When approved:
35+
```bash
36+
<base>/scripts/ralph <fiber-id> [--backend claude|codex] [-- extra-flags...]
37+
```
38+
Add `-- --chrome` for visual/frontend work.
39+
40+
Session: `ralph-<fiber-id>`. Attach: `tmux attach -t ralph-<fiber-id>`.
41+
42+
## Crafting Constitutions
43+
44+
### What goes in one
45+
46+
A constitution needs enough structure that an iteration landing cold can orient itself, and enough freedom that it can adapt. Common sections — use what fits, skip what doesn't, add what's missing:
47+
48+
```markdown
49+
## Desired State
50+
What the system looks like when it's done. Invariants, quality bar,
51+
done-conditions. Fence the scope — what to aim for AND what to leave alone.
52+
53+
## Context
54+
File paths, existing patterns, architectural constraints. Things iterations
55+
need to *find* but not *achieve*.
56+
57+
## Skills
58+
Which skills to activate before working (e.g., /snakemake).
59+
60+
## Evidence
61+
How to check progress — commands, test suites, grep patterns. Pointers to
62+
the ground truth that iterations measure themselves against.
63+
64+
## Open Questions
65+
Uncertainties the user should weigh in on. Iterations add to this; the user
66+
resolves between loops.
67+
```
68+
69+
### Principles
70+
71+
**Constitution, not plan.** Say what the system looks like when it's right. Never describe the current state — anything that becomes false or irrelevant as work progresses doesn't belong. If a section would be outdated after one iteration, it's a snapshot — replace it with a pointer.
72+
73+
**Pointers, not snapshots.** "Check `grep -r 'old_pattern'`" not "50 files remain." Snapshots go stale; pointers stay valid across iterations. This is the constitutional principle: write what remains true until the work is done.
74+
75+
**Prefer existing systems.** Before designing anything new: can what's there handle this?
76+
77+
**Constraints need reasons.** Bare constraints get creatively circumvented. Include enough *why* that an iteration knows when it applies.
78+
79+
**Scope is a gift.** A clear fence — "only rename, don't refactor" — saves iterations from well-intentioned drift. Explicit scope frees the agent to work confidently within it.
80+
81+
### Constitutions that shape artifacts
82+
83+
Some constitutions don't build code — they shape artifacts like tapestries, documentation, or research narratives. These have different rhythms:
84+
85+
- **The desired state is comprehension, not correctness.** "A reviewer can follow the narrative cold" is harder to test than "all tests pass" — but it's the right bar. Evidence for progress: fewer redundant plots, clearer prose, more natural flow.
86+
- **The artifact continues to grow.** Unlike a refactoring (which finishes), a research tapestry keeps acquiring nodes. The constitution shapes how growth presents itself, not when growth stops.
87+
88+
### Anti-patterns
89+
90+
**Checklists.** "1. Add X, 2. Add Y" — iterations race through without judgment.
91+
92+
**Vague done.** "Make it better" — when does iteration stop?
93+
94+
**Over-specification.** Prescribing *how* instead of *what*. Trust the agent's taste.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/bin/bash
2+
# Run a ralph loop on a fiber
3+
# Usage: ralph <fiber-id> [--backend claude|codex] [-- extra-flags...]
4+
#
5+
# Supports both Claude Code and Codex backends.
6+
# Default: claude. Set RALPH_BACKEND=codex or pass --backend codex.
7+
8+
set -e
9+
10+
FIBER_ID="${1:?Usage: ralph <fiber-id> [--backend claude|codex] [-- extra-flags...]}"
11+
shift
12+
13+
BACKEND="${RALPH_BACKEND:-claude}"
14+
if [[ "$1" == "--backend" ]]; then
15+
BACKEND="$2"
16+
shift 2
17+
fi
18+
19+
EXTRA_FLAGS=""
20+
if [[ "$1" == "--" ]]; then
21+
shift
22+
EXTRA_FLAGS="$*"
23+
fi
24+
25+
FELT="$HOME/go/bin/felt"
26+
SESSION="ralph-$FIBER_ID"
27+
WORK_DIR="$(pwd)"
28+
29+
# Find fiber: project .felt/ first, then ~/loom
30+
FELT_DIR=""
31+
if "$FELT" show "$FIBER_ID" >/dev/null 2>&1; then
32+
FELT_DIR="$WORK_DIR"
33+
elif (cd "$HOME/loom" && "$FELT" show "$FIBER_ID" >/dev/null 2>&1); then
34+
FELT_DIR="$HOME/loom"
35+
else
36+
echo "Fiber not found: $FIBER_ID"
37+
exit 1
38+
fi
39+
40+
# Check fiber has open/active status
41+
if ! (cd "$FELT_DIR" && $FELT show "$FIBER_ID" 2>/dev/null | grep -qiE 'status:.*(open|active)'); then
42+
echo "Fiber $FIBER_ID must have status open or active to loop."
43+
echo " Fix: felt edit $FIBER_ID -s open"
44+
exit 1
45+
fi
46+
47+
# Check if already running
48+
if tmux has-session -t "$SESSION" 2>/dev/null; then
49+
echo "Ralph already running: $SESSION"
50+
echo " Attach: tmux attach -t $SESSION"
51+
exit 0
52+
fi
53+
54+
# Write loop script to temp file (avoids heredoc quoting hell)
55+
LOOP_SCRIPT=$(mktemp /tmp/ralph-loop-XXXXXX.sh)
56+
cat > "$LOOP_SCRIPT" << 'LOOP'
57+
#!/bin/bash
58+
FIBER_ID="$1"
59+
FELT_DIR="$2"
60+
WORK_DIR="$3"
61+
BACKEND="$4"
62+
EXTRA_FLAGS="$5"
63+
FELT="$HOME/go/bin/felt"
64+
65+
iteration=0
66+
67+
while (cd "$FELT_DIR" && $FELT show "$FIBER_ID" 2>/dev/null | grep -qiE 'status:.*(open|active)'); do
68+
cd "$WORK_DIR"
69+
iteration=$((iteration + 1))
70+
echo ""
71+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
72+
echo "Ralph iteration $iteration — $(date '+%H:%M:%S')"
73+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
74+
75+
FIBER_CONTENT=$(cd "$FELT_DIR" && $FELT show "$FIBER_ID")
76+
77+
SYSPROMPT_FILE=$(mktemp /tmp/ralph-sys-XXXXXX.txt)
78+
PROMPT_FILE=$(mktemp /tmp/ralph-prompt-XXXXXX.txt)
79+
80+
cat > "$SYSPROMPT_FILE" << SYSEOF
81+
Ralph iteration $iteration. Fiber ID: $FIBER_ID
82+
83+
$FIBER_CONTENT
84+
SYSEOF
85+
86+
cat > "$PROMPT_FILE" << 'PROMPTEOF'
87+
You are inside a ralph loop. Activate the ralph skill (/ralph) and follow its instructions for iterating on the constitution above.
88+
PROMPTEOF
89+
90+
PROMPT=$(cat "$PROMPT_FILE")
91+
92+
if [[ "$BACKEND" == "codex" ]]; then
93+
codex --dangerously-bypass-approvals-and-sandbox \
94+
--config "developer_instructions=$(cat "$SYSPROMPT_FILE")" \
95+
$EXTRA_FLAGS \
96+
"$PROMPT"
97+
else
98+
claude --dangerously-skip-permissions \
99+
$EXTRA_FLAGS \
100+
--append-system-prompt "$(cat "$SYSPROMPT_FILE")" \
101+
<<< "$PROMPT"
102+
fi
103+
104+
rm -f "$SYSPROMPT_FILE" "$PROMPT_FILE"
105+
106+
echo "--- Iteration complete ---"
107+
sleep 2
108+
done
109+
110+
echo ""
111+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
112+
echo "Ralph complete — $iteration iterations"
113+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
114+
echo ""
115+
echo "Session kept open for inspection. Type exit to close."
116+
exec bash -l
117+
LOOP
118+
119+
chmod +x "$LOOP_SCRIPT"
120+
121+
echo "Starting ralph on $FIBER_ID"
122+
echo " Work dir: $WORK_DIR"
123+
echo " Fiber in: $FELT_DIR"
124+
[[ -n "$EXTRA_FLAGS" ]] && echo " Flags: $EXTRA_FLAGS"
125+
126+
# Launch tmux with a login shell running the loop script
127+
tmux new-session -d -s "$SESSION" -c "$WORK_DIR" \
128+
bash -l "$LOOP_SCRIPT" "$FIBER_ID" "$FELT_DIR" "$WORK_DIR" "$BACKEND" "$EXTRA_FLAGS"
129+
130+
echo " Session: $SESSION"
131+
echo " Attach: tmux attach -t $SESSION"

0 commit comments

Comments
 (0)