tiller-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +138 -0
  2. package/dist/index.js +1183 -0
  3. package/package.json +47 -0
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # tiller-ai
2
+
3
+ Scaffold Claude Code projects with a structured vibe loop — branch, build, commit, land.
4
+
5
+ ## What is this?
6
+
7
+ Tiller is a thin scaffold for Claude Code that turns a blank repo into a project Claude knows how to navigate. It installs a set of slash commands (skills), two `CLAUDE.md` files (one user-facing, one Tiller-managed), hooks for formatting and secret scanning, and shared tracking files. Once scaffolded, you describe work with `/vibe`, save checkpoints with `/save`, and ship with `/land` — and Claude follows the loop without you having to re-explain your workflow every session.
8
+
9
+ ## Quick start
10
+
11
+ ```bash
12
+ # 1. Scaffold a new project
13
+ npx tiller-ai init
14
+
15
+ # 2. Open in Claude Code and run setup
16
+ /setup
17
+
18
+ # 3. Start working
19
+ /vibe add a login page
20
+ ```
21
+
22
+ ## Skills
23
+
24
+ | Command | What it does |
25
+ |---|---|
26
+ | `/setup` | First-run: understand the project and fill in `CLAUDE.md` |
27
+ | `/vibe [idea]` | Start or continue work on something |
28
+ | `/save` | Commit current progress on the feature branch |
29
+ | `/land` | Merge completed feature to main (solo) or open a PR (team) |
30
+ | `/recap` | Read-only status — active feature, notes |
31
+
32
+ ## Modes
33
+
34
+ Tiller has two modes that control how Claude communicates:
35
+
36
+ **`simple`** — for non-technical users. Claude builds without narrating steps, surfaces only blockers, and keeps responses short and outcome-focused.
37
+
38
+ **`detailed`** — for technical users. Claude proposes an approach and waits for confirmation before touching files, narrates decisions, and surfaces trade-offs.
39
+
40
+ Switch modes at any time:
41
+
42
+ ```bash
43
+ npx tiller-ai mode simple
44
+ npx tiller-ai mode detailed
45
+ ```
46
+
47
+ Use `--project` to update the shared project default instead of your personal override:
48
+
49
+ ```bash
50
+ npx tiller-ai mode detailed --project
51
+ ```
52
+
53
+ Or just update the `Mode:` line in your root `CLAUDE.md`.
54
+
55
+ ## Workflows
56
+
57
+ Tiller supports two workflows that affect how `/land` behaves:
58
+
59
+ **`solo`** — single developer. `/land` merges the feature branch into main locally and deletes the branch.
60
+
61
+ **`team`** — multiple developers. `/land` pushes the branch and opens a PR (via `gh` CLI if available, otherwise prints the URL). The branch is not deleted locally.
62
+
63
+ The workflow is set during `init` and stored in `.claude/.tiller.json`. Each developer can override it locally in `.tiller.local.json` (gitignored).
64
+
65
+ ## What gets scaffolded
66
+
67
+ ```
68
+ your-project/
69
+ ├── CLAUDE.md # User-facing: project context, verify command, mode, workflow
70
+ ├── .gitignore # Ignores vibestate.md, .tiller.local.json, common build artifacts
71
+ ├── changelog.md # Shared done log — updated by /land on each merge
72
+ ├── vibestate.md # Per-dev: active feature, milestone checklist, notes (gitignored)
73
+ ├── .claude/
74
+ │ ├── CLAUDE.md # Tiller-managed: vibe loop rules, skill docs
75
+ │ ├── settings.json # Hook registrations (PostToolUse, PreToolUse, UserPromptSubmit)
76
+ │ ├── .tiller.json # Manifest: version, mode, workflow, runCommand, managedFiles
77
+ │ ├── hooks/
78
+ │ │ ├── post-write.sh # PostToolUse: run formatter after file writes
79
+ │ │ ├── secret-scan.sh # PreToolUse: block writes containing secrets
80
+ │ │ └── session-resume.sh # UserPromptSubmit: orient Claude at session start
81
+ │ └── skills/
82
+ │ ├── setup/SKILL.md # /setup skill
83
+ │ ├── vibe/SKILL.md # /vibe skill
84
+ │ ├── save/SKILL.md # /save skill
85
+ │ ├── land/SKILL.md # /land skill
86
+ │ └── recap/SKILL.md # /recap skill
87
+ ```
88
+
89
+ **Per-dev local overrides** — `.tiller.local.json` (gitignored, not scaffolded) lets individual developers override `mode` and `workflow` without touching shared files.
90
+
91
+ ## The vibe loop
92
+
93
+ Every piece of work follows this loop:
94
+
95
+ 1. **Orient** — Claude reads `CLAUDE.md` and `vibestate.md` to understand project state and pick up any in-progress work
96
+ 2. **Confirm** — in `detailed` mode, Claude writes out the proposed approach and waits for a go-ahead before touching files
97
+ 3. **Build** — Claude implements, runs the verify command after each chunk, and fixes failures before moving on
98
+ 4. **Save** — Claude reminds you to `/save` when stable and `/land` when the feature is done
99
+
100
+ `vibestate.md` tracks the active branch, milestone checklist, and session notes. `changelog.md` is the shared done log — updated by `/land` whenever a feature merges, so team members can see what's been shipped.
101
+
102
+ ## CLI reference
103
+
104
+ ### `npx tiller-ai init`
105
+
106
+ Scaffold a new project interactively. Prompts for project name, description, run/verify command, mode, and workflow. Writes all files and makes an initial git commit.
107
+
108
+ ```bash
109
+ npx tiller-ai init
110
+ ```
111
+
112
+ ### `npx tiller-ai upgrade`
113
+
114
+ Update Tiller-managed files (`.claude/CLAUDE.md`, `settings.json`, hooks, skills) to the latest version without touching your `CLAUDE.md`, `vibestate.md`, or `changelog.md`.
115
+
116
+ ```bash
117
+ npx tiller-ai upgrade
118
+ ```
119
+
120
+ ### `npx tiller-ai mode <mode>`
121
+
122
+ Switch the project mode between `simple` and `detailed`. Without `--project`, writes to `.tiller.local.json` (your personal override). With `--project`, updates the shared `CLAUDE.md`.
123
+
124
+ ```bash
125
+ npx tiller-ai mode simple
126
+ npx tiller-ai mode detailed
127
+ npx tiller-ai mode detailed --project # update shared project default
128
+ ```
129
+
130
+ ## Requirements
131
+
132
+ - Node 22+
133
+ - [Claude Code](https://claude.ai/code)
134
+ - git
135
+
136
+ ## License
137
+
138
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,1183 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import { intro, outro, spinner } from "@clack/prompts";
8
+ import { resolve, basename } from "path";
9
+
10
+ // src/scaffold/index.ts
11
+ import { join as join2 } from "path";
12
+
13
+ // src/utils/fs.ts
14
+ import { mkdir, writeFile as fsWriteFile } from "fs/promises";
15
+ import { dirname } from "path";
16
+ async function ensureDir(dirPath) {
17
+ await mkdir(dirPath, { recursive: true });
18
+ }
19
+ async function writeFile(filePath, content) {
20
+ await ensureDir(dirname(filePath));
21
+ await fsWriteFile(filePath, content, "utf-8");
22
+ }
23
+
24
+ // src/utils/git.ts
25
+ import { execSync } from "child_process";
26
+ import { existsSync } from "fs";
27
+ import { join } from "path";
28
+ function exec(cmd, cwd) {
29
+ execSync(cmd, { cwd, stdio: "pipe" });
30
+ }
31
+ function isGitRepo(cwd) {
32
+ return existsSync(join(cwd, ".git"));
33
+ }
34
+ function gitInit(cwd) {
35
+ exec("git init", cwd);
36
+ try {
37
+ exec("git checkout -b main", cwd);
38
+ } catch {
39
+ }
40
+ }
41
+ function gitAdd(cwd, pattern = "-A") {
42
+ exec(`git add ${pattern}`, cwd);
43
+ }
44
+ function gitCommit(cwd, message) {
45
+ exec(`git commit -m "${message.replace(/"/g, '\\"')}"`, cwd);
46
+ }
47
+
48
+ // src/scaffold/claude-md.ts
49
+ function generateRootClaudeMd(config) {
50
+ const description = config.description || "<!-- Run /setup to fill this in -->";
51
+ const runCommand = config.runCommand || "# not set \u2014 run /setup to configure";
52
+ const modeLabel = config.mode === "detailed" ? "detailed" : "simple";
53
+ const workflowLabel = config.workflow === "team" ? "team" : "solo";
54
+ return `# ${config.projectName}
55
+
56
+ ${description}
57
+
58
+ ## Verify command
59
+
60
+ \`\`\`
61
+ ${runCommand}
62
+ \`\`\`
63
+
64
+ ## Mode
65
+
66
+ ${modeLabel}
67
+
68
+ ## Workflow
69
+
70
+ ${workflowLabel}
71
+ `;
72
+ }
73
+ function generateDotClaudeMd(_config) {
74
+ return `# Tiller \u2014 How to work
75
+
76
+ > These rules are managed by Tiller. Do not edit manually.
77
+
78
+ ## Modes
79
+
80
+ The mode is set in CLAUDE.md (or overridden locally in \`.tiller.local.json\`). Read it at the start of every session.
81
+
82
+ ### simple
83
+
84
+ The user is non-technical. They want to describe what they want and have it built for them.
85
+
86
+ - Do not explain your technical decisions unless asked
87
+ - Do not narrate steps as you do them
88
+ - Do not ask about tooling, frameworks, file structure, or verify commands
89
+ - When something goes wrong, fix it yourself first \u2014 only surface it if you can't resolve it
90
+ - Keep all communication short and outcome-focused: "Done. Here's what changed."
91
+
92
+ ### detailed
93
+
94
+ The user is technical and wants to stay in control.
95
+
96
+ - Before touching files: write out your proposed approach, list files you'll create or modify, wait for explicit confirmation
97
+ - Narrate what you're doing and why
98
+ - Surface decisions and trade-offs before making them
99
+ - When something goes wrong, explain what happened and what you plan to do
100
+
101
+ ## Workflows
102
+
103
+ The workflow is set in CLAUDE.md (or overridden locally in \`.tiller.local.json\`).
104
+
105
+ ### solo
106
+
107
+ Single developer or local-only flow. /land merges directly to main.
108
+
109
+ ### team
110
+
111
+ Multiple developers. /land pushes the feature branch and opens a PR. Merging happens on GitHub/GitLab after review and CI.
112
+
113
+ ## Vibe loop
114
+
115
+ Every piece of work follows this loop:
116
+
117
+ 1. **Orient** \u2014 read CLAUDE.md, vibestate.md (local), and changelog.md (shared)
118
+ 2. **Confirm** \u2014 in detailed mode, enter plan mode with milestones and wait for approval
119
+ 3. **Build** \u2014 implement milestone by milestone; each milestone includes tests, verify, and auto-commit
120
+ 4. **Complete** \u2014 announce feature done, suggest /land
121
+
122
+ ## File discipline
123
+
124
+ - Never commit directly to main
125
+ - Always work on a feature branch (feature/<name>)
126
+ - Run the verify command before every snapshot and land
127
+ - \`vibestate.md\` is gitignored \u2014 it tracks your local active feature state
128
+ - \`changelog.md\` is committed and shared \u2014 it tracks the project's done log
129
+
130
+ ## Per-dev overrides
131
+
132
+ Create \`.tiller.local.json\` (gitignored) to override mode or workflow personally:
133
+ \`\`\`json
134
+ { "mode": "simple", "workflow": "solo" }
135
+ \`\`\`
136
+ Skills read \`.tiller.local.json\` first, then fall back to CLAUDE.md.
137
+
138
+ ## Skills
139
+
140
+ - **/setup** \u2014 first-run: understand the project and configure CLAUDE.md
141
+ - **/vibe** [idea] \u2014 milestone-driven development: plan, build, test, auto-commit. Every 3 landed features, automatically runs a background tech debt cleanup before planning.
142
+ - **/snapshot** \u2014 commit current progress on the feature branch
143
+ - **/land** \u2014 merge or PR depending on workflow
144
+ - **/recap** \u2014 read-only status of all work
145
+
146
+ ## Rules
147
+
148
+ - Do not skip the verify step
149
+ - Do not touch unrelated files
150
+ - Do not make architectural changes without explicit confirmation in detailed mode
151
+ - Keep commits atomic and descriptive
152
+ `;
153
+ }
154
+
155
+ // src/scaffold/vibestate.ts
156
+ function generateVibestate(_config) {
157
+ return `# vibestate.md \u2014 local only, do not commit
158
+
159
+ > Tracks your active feature work. Gitignored \u2014 each dev has their own copy.
160
+
161
+ ## Active feature
162
+
163
+ None \u2014 on main, ready to start something.
164
+
165
+ ## Notes
166
+
167
+ <!-- Add context, gotchas, decisions here -->
168
+ `;
169
+ }
170
+
171
+ // src/scaffold/changelog.ts
172
+ function generateChangelog(config) {
173
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
174
+ return `# changelog.md \u2014 ${config.projectName}
175
+
176
+ > Shared project history. Updated by /vibe, /snapshot, and /land. Committed and shared.
177
+
178
+ ## Done
179
+
180
+ - [${today}] v0 \u2014 initial scaffold
181
+ `;
182
+ }
183
+
184
+ // src/scaffold/settings-json.ts
185
+ function generateSettingsJson(_config) {
186
+ const settings = {
187
+ hooks: {
188
+ PostToolUse: [
189
+ {
190
+ matcher: "Edit|Write|MultiEdit",
191
+ hooks: [
192
+ {
193
+ type: "command",
194
+ command: 'bash .claude/hooks/post-write.sh "$CLAUDE_FILE_PATHS"'
195
+ }
196
+ ]
197
+ }
198
+ ],
199
+ PreToolUse: [
200
+ {
201
+ matcher: "Bash",
202
+ hooks: [
203
+ {
204
+ type: "command",
205
+ command: "bash .claude/hooks/secret-scan.sh"
206
+ }
207
+ ]
208
+ }
209
+ ],
210
+ SessionStart: [
211
+ {
212
+ matcher: "clear",
213
+ hooks: [
214
+ {
215
+ type: "command",
216
+ command: "bash .claude/hooks/session-resume.sh"
217
+ }
218
+ ]
219
+ }
220
+ ]
221
+ },
222
+ permissions: {
223
+ allow: [
224
+ "Bash(git:*)",
225
+ "Bash(npm:*)",
226
+ "Bash(npx:*)",
227
+ "Bash(node:*)",
228
+ "Bash(echo:*)"
229
+ ]
230
+ }
231
+ };
232
+ return JSON.stringify(settings, null, 2);
233
+ }
234
+
235
+ // src/scaffold/gitignore.ts
236
+ function generateGitignore(_config) {
237
+ return `# Dependencies
238
+ node_modules/
239
+
240
+ # Build output
241
+ dist/
242
+ build/
243
+ .next/
244
+ out/
245
+
246
+ # Environment
247
+ .env
248
+ .env.local
249
+ .env.*.local
250
+
251
+ # Editor
252
+ .DS_Store
253
+ *.swp
254
+ *.swo
255
+ .idea/
256
+ .vscode/
257
+
258
+ # Logs
259
+ *.log
260
+ npm-debug.log*
261
+
262
+ # TypeScript
263
+ *.tsbuildinfo
264
+
265
+ # Tiller \u2014 local-only files (per-dev, not shared)
266
+ vibestate.md
267
+ .tiller.local.json
268
+ `;
269
+ }
270
+
271
+ // src/scaffold/tiller-manifest.ts
272
+ var MANAGED_FILES = [
273
+ ".claude/CLAUDE.md",
274
+ ".claude/settings.json",
275
+ ".claude/hooks/post-write.sh",
276
+ ".claude/hooks/secret-scan.sh",
277
+ ".claude/skills/setup/SKILL.md",
278
+ ".claude/skills/vibe/SKILL.md",
279
+ ".claude/skills/snapshot/SKILL.md",
280
+ ".claude/skills/recap/SKILL.md",
281
+ ".claude/skills/land/SKILL.md",
282
+ ".claude/skills/tech-debt/SKILL.md"
283
+ ];
284
+ function generateTillerManifest(config, version) {
285
+ const manifest = {
286
+ version,
287
+ mode: config.mode,
288
+ workflow: config.workflow,
289
+ runCommand: config.runCommand,
290
+ managedFiles: MANAGED_FILES
291
+ };
292
+ return JSON.stringify(manifest, null, 2);
293
+ }
294
+
295
+ // src/scaffold/hooks/post-write.ts
296
+ function generatePostWriteHook(_config) {
297
+ return `#!/usr/bin/env bash
298
+ # post-write.sh \u2014 auto-format after file edits
299
+ # Managed by Tiller. Silent fail if formatter not available.
300
+
301
+ set -euo pipefail
302
+
303
+ FILES="$1"
304
+
305
+ if [ -z "$FILES" ]; then
306
+ exit 0
307
+ fi
308
+
309
+ # Try prettier
310
+ if command -v prettier &>/dev/null; then
311
+ echo "$FILES" | tr ' ' '\\n' | xargs prettier --write --ignore-unknown 2>/dev/null || true
312
+ exit 0
313
+ fi
314
+
315
+ # Try biome
316
+ if command -v biome &>/dev/null; then
317
+ echo "$FILES" | tr ' ' '\\n' | xargs biome format --write 2>/dev/null || true
318
+ exit 0
319
+ fi
320
+
321
+ # No formatter found \u2014 that's fine
322
+ exit 0
323
+ `;
324
+ }
325
+
326
+ // src/scaffold/hooks/secret-scan.ts
327
+ function generateSecretScanHook(_config) {
328
+ return `#!/usr/bin/env bash
329
+ # secret-scan.sh \u2014 block commits that contain secrets
330
+ # Managed by Tiller. Called as PreToolUse hook on Bash commands.
331
+
332
+ set -euo pipefail
333
+
334
+ # Read the tool input from stdin
335
+ INPUT=$(cat)
336
+
337
+ # Only run on git commit commands
338
+ COMMAND=$(echo "$INPUT" | python3 -c "import sys, json; data = json.load(sys.stdin); print(data.get('command', ''))" 2>/dev/null || echo "")
339
+
340
+ if [[ "$COMMAND" != *"git commit"* ]]; then
341
+ exit 0
342
+ fi
343
+
344
+ # Get staged files
345
+ STAGED=$(git diff --cached --name-only 2>/dev/null || true)
346
+
347
+ if [ -z "$STAGED" ]; then
348
+ exit 0
349
+ fi
350
+
351
+ # Patterns that look like secrets
352
+ SECRET_PATTERNS=(
353
+ 'sk-[a-zA-Z0-9]{20,}'
354
+ 'ghp_[a-zA-Z0-9]{36}'
355
+ 'gho_[a-zA-Z0-9]{36}'
356
+ 'github_pat_[a-zA-Z0-9_]{82}'
357
+ 'xoxb-[0-9]+-[a-zA-Z0-9]+'
358
+ 'AKIA[0-9A-Z]{16}'
359
+ 'eyJ[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+'
360
+ 'AIza[0-9A-Za-z_-]{35}'
361
+ 'password\\s*=\\s*["\\'\\''"][^"\\'']+["\\'\\''"]'
362
+ 'secret\\s*=\\s*["\\'\\''"][^"\\'']+["\\'\\''"]'
363
+ 'api[_-]?key\\s*=\\s*["\\'\\''"][^"\\'']+["\\'\\''"]'
364
+ )
365
+
366
+ FOUND=""
367
+ for PATTERN in "\${SECRET_PATTERNS[@]}"; do
368
+ MATCHES=$(git diff --cached | grep -iE "$PATTERN" 2>/dev/null || true)
369
+ if [ -n "$MATCHES" ]; then
370
+ FOUND="$FOUND\\n$MATCHES"
371
+ fi
372
+ done
373
+
374
+ if [ -n "$FOUND" ]; then
375
+ echo '{"permissionDecision":"deny","denyReason":"Potential secrets detected in staged changes. Remove secrets before committing."}'
376
+ exit 0
377
+ fi
378
+
379
+ exit 0
380
+ `;
381
+ }
382
+
383
+ // src/scaffold/hooks/session-resume.ts
384
+ function generateSessionResumeHook(_config) {
385
+ return `#!/usr/bin/env bash
386
+ # Injected into context on SessionStart(clear)
387
+ if grep -q "Status: executing" vibestate.md 2>/dev/null; then
388
+ PLAN_FILE=$(grep "Plan:" vibestate.md | head -1 | sed 's/Plan: //')
389
+ echo "You are in the middle of a /vibe milestone build."
390
+ echo "Read vibestate.md for milestone progress and \${PLAN_FILE:-the plan file} for full context."
391
+ echo "Resume the milestone loop from where you left off. Auto-commit after each milestone."
392
+ fi
393
+ `;
394
+ }
395
+
396
+ // src/scaffold/skills/vibe.ts
397
+ function generateVibeSkill(config) {
398
+ return `---
399
+ name: vibe
400
+ description: Start or continue working on an idea. Usage: /vibe [idea description]
401
+ ---
402
+
403
+ # /vibe \u2014 Start or continue work
404
+
405
+ ## Step 1: Orient
406
+
407
+ Read \`CLAUDE.md\`, \`vibestate.md\`, and \`changelog.md\` to understand current state.
408
+ Run \`git branch\` and \`git status\`.
409
+
410
+ State the current mode from CLAUDE.md: "Mode: <mode>".
411
+
412
+ **If mode is simple:** Do not narrate the orient step.
413
+ **If mode is detailed:** Summarize the current state in 2-3 sentences.
414
+
415
+ ## Step 2: Branch routing
416
+
417
+ **$ARGUMENTS provided** \u2192 check if a branch named \`feature/<kebab-case-of-arguments>\` already exists locally or remotely.
418
+ - If it exists: switch to it. Read \`vibestate.md\` for current state. Ask: "Found existing branch feature/<name>. Continue where we left off, or do you want to revisit the plan first?"
419
+ - Continue \u2192 pick up from the next unchecked milestone
420
+ - Revisit \u2192 summarize what's done so far, discuss before building
421
+ - If it doesn't exist: create it from main.
422
+ - **simple:** Say: "On it."
423
+ - **detailed:** State: "Starting work on: <idea>"
424
+
425
+ **Already on a feature branch** \u2192 continue.
426
+ - **simple:** Say nothing unless asked.
427
+ - **detailed:** State: "Continuing work on: <branch-name>"
428
+
429
+ **Neither** \u2192 list open feature branches briefly, ask what to work on.
430
+
431
+ ## Step 2.5: Tech debt check
432
+
433
+ Before planning, check if a tech debt cleanup is due:
434
+
435
+ 1. Count lines in \`changelog.md\` matching the pattern \`- [[^]]*] landed feature/\` \u2014 this is \`landedCount\`
436
+ 2. Read \`.claude/.tiller-tech-debt.json\` \u2014 get \`lastTechDebtAtFeature\` and \`threshold\` (default threshold: 3)
437
+ 3. If \`(landedCount - lastTechDebtAtFeature) >= threshold\`:
438
+ - Use the **Task tool** (foreground, \`subagent_type: "general-purpose"\`) with the contents of \`.claude/skills/tech-debt/SKILL.md\` as the prompt
439
+ - Wait for the agent to complete before continuing
440
+ 4. Continue to Step 3 regardless of whether the tech debt agent ran
441
+
442
+ ## Step 3: Plan milestones
443
+
444
+ **If mode is simple:** Explore the codebase and break the work into 2\u20135 milestones internally. Do not show this plan to the user.
445
+
446
+ **If mode is detailed:** Call \`EnterPlanMode\`. In the plan file, write:
447
+ - High-level approach (2\u20133 sentences)
448
+ - 2\u20135 numbered milestones, each with: what gets built + what gets tested
449
+ - Files to create or modify
450
+ - Any trade-offs worth noting
451
+ - **Execution rules** (embed verbatim): After plan approval, read \`vibestate.md\` to find the milestone checklist, then execute the milestone loop: for each remaining milestone, announce "Milestone X/N: <description>", build functionality, add or update tests, run \`${config.runCommand}\` and fix failures, run \`git add -A && git commit -m "<milestone>"\`, update \`vibestate.md\` checkboxes and \`changelog.md\` Done section then amend commit, report "Saved: <description> (X/N)". When all milestones are done, summarize what was built and suggest \`/land\`.
452
+
453
+ Before exiting plan mode, write the milestone checklist to the \`Active feature\` section of \`vibestate.md\` with \`Status: executing\` and the plan file path.
454
+
455
+ ## Step 4: Build milestone by milestone
456
+
457
+ For each milestone:
458
+ 1. **detailed only:** Announce: "Milestone X/N: <description>"
459
+ 2. Build the functionality
460
+ 3. Add or update tests for what was built
461
+ 4. Run \`${config.runCommand}\` \u2014 **simple:** fix failures silently. **detailed:** fix before continuing.
462
+ 5. \`git add -A && git commit -m "<milestone description>"\`
463
+ 6. Update \`vibestate.md\` milestone checkboxes (detailed) and add entry to \`changelog.md\` Done section. Amend: \`git commit --amend --no-edit\`
464
+ 7. **simple:** Say: "Saved: <what changed>". **detailed:** Report: "Saved: <description> (X/N)"
465
+
466
+ ## Step 5: Complete
467
+
468
+ **simple:** Say: "Feature complete. Type /land when ready to merge."
469
+ **detailed:** Summarize everything that was built across all milestones. Suggest \`/land\` to merge.
470
+
471
+ ## If something goes wrong
472
+
473
+ **simple:** Fix it yourself first. Only tell the user if you genuinely can't resolve it.
474
+ **detailed:** Explain what happened and what you plan to do.
475
+ `;
476
+ }
477
+
478
+ // src/scaffold/skills/snapshot.ts
479
+ function generateSnapshotSkill(config) {
480
+ return `---
481
+ name: snapshot
482
+ description: Save current progress with a commit on the feature branch
483
+ ---
484
+
485
+ # /snapshot \u2014 Save progress
486
+
487
+ ## Step 1: Check branch
488
+
489
+ Run \`git branch --show-current\`.
490
+
491
+ If on \`main\`:
492
+ - **simple:** Say: "You're on main \u2014 use /vibe to start a feature first." Stop.
493
+ - **detailed:** Warn: "You're on main. Snapshot is for feature branches. Use /vibe to start a feature branch first." Stop.
494
+
495
+ ## Step 2: Run verify
496
+
497
+ Run \`${config.runCommand}\`
498
+
499
+ If it fails:
500
+ - **simple:** Say: "Something's broken, let me fix it first." Fix it, then continue.
501
+ - **detailed:** Show the error output. Do NOT commit. Say: "Verify failed. Fix the errors and try /snapshot again." Stop.
502
+
503
+ ## Step 3: Describe changes
504
+
505
+ If $ARGUMENTS is provided, use that as the commit message.
506
+
507
+ Otherwise, run \`git diff --stat HEAD\` and infer a short, descriptive commit message.
508
+
509
+ Format: \`<verb> <what> \u2014 <brief detail if needed>\`
510
+
511
+ ## Step 4: Commit
512
+
513
+ \`\`\`
514
+ git add -A
515
+ git commit -m "<message>"
516
+ \`\`\`
517
+
518
+ ## Step 5: Update changelog.md
519
+
520
+ Add an entry to the Done section of \`changelog.md\`:
521
+ \`\`\`
522
+ - [YYYY-MM-DD] <message>
523
+ \`\`\`
524
+
525
+ Run \`git add changelog.md && git commit --amend --no-edit\`.
526
+
527
+ ## Step 6: Confirm
528
+
529
+ - **simple:** Say: "Saved. Keep going or type /land when you're done."
530
+ - **detailed:** Say: "Snapshot saved: <message>. Use /land when this feature is ready to merge."
531
+ `;
532
+ }
533
+
534
+ // src/scaffold/skills/recap.ts
535
+ function generateRecapSkill(_config) {
536
+ return `---
537
+ name: recap
538
+ description: Read-only status of all work \u2014 completed and in progress
539
+ ---
540
+
541
+ # /recap \u2014 Project status
542
+
543
+ > Read-only. No file modifications. No suggestions. No internal monologue.
544
+
545
+ ## Gather everything silently first
546
+
547
+ Run all of these before writing any output:
548
+
549
+ 1. Read \`vibestate.md\` (active feature, local state)
550
+ 2. Read \`changelog.md\` (shared done log)
551
+ 3. \`git log main --oneline\`
552
+ 4. \`git branch --list 'feature/*'\`
553
+ 5. For each feature branch: \`git log main..<branch> --oneline\`
554
+
555
+ ## Then produce output based on mode
556
+
557
+ Read mode from CLAUDE.md.
558
+
559
+ **If mode is simple:** Translate everything into plain English. No hashes, no branch names, no git jargon.
560
+
561
+ Format:
562
+
563
+ ---
564
+
565
+ **Working on:** <what's currently being built, from vibestate.md \u2014 or "nothing, ready to start">
566
+
567
+ **Done**
568
+ - <plain English description of what was built>
569
+ ...
570
+
571
+ **In progress**
572
+ - <plain English description of what's being worked on>
573
+ ...
574
+
575
+ ---
576
+
577
+ If there's nothing in progress: omit the "In progress" section.
578
+ If there's nothing done yet: say "Nothing completed yet."
579
+
580
+ **If mode is detailed:** Include technical details.
581
+
582
+ Format:
583
+
584
+ ---
585
+
586
+ **Active:** <active feature from vibestate.md, or "none">
587
+
588
+ **Completed (main)**
589
+ <hash> <message>
590
+ ...
591
+
592
+ **In progress**
593
+ feature/<name>
594
+ <hash> <message>
595
+ ...
596
+
597
+ <X> features landed, <Y> in progress.
598
+
599
+ ---
600
+
601
+ Nothing else. No commentary, no second-guessing, no re-running commands.
602
+ `;
603
+ }
604
+
605
+ // src/scaffold/skills/land.ts
606
+ function generateLandSkill(config) {
607
+ return `---
608
+ name: land
609
+ description: Merge completed feature to main and clean up the branch
610
+ ---
611
+
612
+ # /land \u2014 Merge feature to main
613
+
614
+ ## Step 1: Check branch
615
+
616
+ Run \`git branch --show-current\`.
617
+
618
+ If on \`main\`:
619
+ - **simple:** Say: "You're already on main." Stop.
620
+ - **detailed:** Error: "You're already on main. Switch to the feature branch you want to land." Stop.
621
+
622
+ Save the current branch name as \`<feature-branch>\`.
623
+
624
+ ## Step 2: Run verify
625
+
626
+ Run \`${config.runCommand}\`
627
+
628
+ If it fails:
629
+ - **simple:** Say: "Something's not working, let me sort it out." Fix it first.
630
+ - **detailed:** Show the error output. Do NOT proceed. Say: "Verify failed. Fix the errors and try /land again." Stop.
631
+
632
+ ## Step 3: Commit any uncommitted changes
633
+
634
+ Run \`git status --porcelain\`.
635
+
636
+ If there are uncommitted changes:
637
+ \`\`\`
638
+ git add -A
639
+ git commit -m "wip: save before landing"
640
+ \`\`\`
641
+
642
+ ## Step 4: Check workflow
643
+
644
+ Read workflow from \`.tiller.local.json\` if it exists, otherwise from CLAUDE.md or \`.tiller.json\`. Default: solo.
645
+
646
+ **If workflow is solo** \u2192 go to Step 5a (local merge).
647
+ **If workflow is team** \u2192 go to Step 5b (open PR).
648
+
649
+ ## Step 5a: Solo \u2014 merge to main
650
+
651
+ \`\`\`
652
+ git checkout main
653
+ git merge --no-ff <feature-branch> -m "land: <feature-branch>"
654
+ git branch -d <feature-branch>
655
+ \`\`\`
656
+
657
+ Then go to Step 6.
658
+
659
+ ## Step 5b: Team \u2014 open PR
660
+
661
+ Push the branch:
662
+ \`\`\`
663
+ git push origin <feature-branch>
664
+ \`\`\`
665
+
666
+ Check if \`gh\` CLI is available: run \`which gh\`.
667
+
668
+ **If gh is available:**
669
+ \`\`\`
670
+ gh pr create --fill
671
+ \`\`\`
672
+ Print the PR URL. Say: "PR opened. Merge happens on GitHub after review and CI."
673
+
674
+ **If gh is not available:**
675
+ Run \`git remote get-url origin\` to get the remote URL. Convert to a browser URL if needed.
676
+ Say: "Push done. Open a PR at: <remote-url>/compare/<feature-branch>"
677
+
678
+ Then go to Step 6 (do NOT delete the branch locally \u2014 it will be deleted after the PR merges remotely).
679
+
680
+ ## Step 6: Update changelog.md
681
+
682
+ 1. Add an entry to the Done section of \`changelog.md\`:
683
+ - **solo:** \`- [YYYY-MM-DD] landed <feature-branch>\`
684
+ - **team:** \`- [YYYY-MM-DD] PR opened: <feature-branch>\`
685
+ 2. Clear the \`Active feature\` section of \`vibestate.md\`: set it to "None \u2014 on main, ready to start something."
686
+ 3. Commit:
687
+ \`\`\`
688
+ git add changelog.md vibestate.md && git commit -m "update changelog: landed <feature-branch>"
689
+ \`\`\`
690
+ **team:** commit to the feature branch before pushing (or amend if already pushed).
691
+
692
+ ## Step 7: Confirm
693
+
694
+ - **simple:** Say: "Done. Run \`/clear\` to reset context before starting your next feature, then \`/vibe\` to continue."
695
+ - **detailed:** Say: "Feature landed on main. Run \`/clear\` to reset context before your next feature, then \`/vibe\` to continue."
696
+ `;
697
+ }
698
+
699
+ // src/scaffold/skills/setup.ts
700
+ function generateSetupSkill(_config) {
701
+ return `---
702
+ name: setup
703
+ description: First-run setup \u2014 understand the project and configure CLAUDE.md
704
+ ---
705
+
706
+ # /setup \u2014 Configure this project
707
+
708
+ > Run this once after \`tiller-ai init\`.
709
+
710
+ ## Step 1: Ask about mode first
711
+
712
+ Ask only this question to start:
713
+
714
+ > "How do you want to work together?
715
+ > - **Simple** \u2014 just tell me what to build, I'll handle all the technical decisions.
716
+ > - **Detailed** \u2014 I'll explain my thinking and check in with you before making decisions."
717
+
718
+ Wait for their answer. Default to simple if they're unsure.
719
+
720
+ ---
721
+
722
+ ## If mode is SIMPLE
723
+
724
+ The user is non-technical. Handle everything yourself, ask as little as possible.
725
+
726
+ **Step 2: Ask what the project is**
727
+
728
+ Ask one question: "What are you building?"
729
+
730
+ Listen to their answer. That's enough \u2014 use it as the description.
731
+
732
+ **Step 3: Derive the verify command yourself**
733
+
734
+ Look at the project files silently. Do not ask the user about this.
735
+
736
+ - \`package.json\` with a \`test\` script \u2192 \`npm test\`
737
+ - \`package.json\` without a \`test\` script \u2192 \`npm run build\` if build script exists, else \`node --check index.js\`
738
+ - \`pyproject.toml\` or \`setup.py\` \u2192 \`pytest\`
739
+ - \`Cargo.toml\` \u2192 \`cargo test\`
740
+ - \`go.mod\` \u2192 \`go test ./...\`
741
+ - \`Makefile\` with a \`test\` target \u2192 \`make test\`
742
+ - Nothing recognizable \u2192 \`echo ok\` (can be updated later)
743
+
744
+ **Step 4: Update CLAUDE.md and commit**
745
+
746
+ Rewrite \`CLAUDE.md\` with the project name, their description, the verify command you derived, and mode: simple.
747
+
748
+ Update \`runCommand\` and \`mode\` in \`.claude/.tiller.json\`.
749
+
750
+ \`\`\`
751
+ git add CLAUDE.md .claude/.tiller.json
752
+ git commit -m "chore: configure project via /setup"
753
+ \`\`\`
754
+
755
+ Say: "All set. Just tell me what you want to build and I'll take it from there."
756
+
757
+ ---
758
+
759
+ ## If mode is DETAILED
760
+
761
+ The user is technical and wants to stay in the loop.
762
+
763
+ **Step 2: Ask about the project**
764
+
765
+ Ask: "What are you building?" \u2014 let them describe it.
766
+
767
+ If there's already a README or package.json, read it first and confirm your understanding instead of asking blind.
768
+
769
+ **Step 3: Ask about the verify command**
770
+
771
+ Ask: "What command should I run to verify everything's working after a change?"
772
+
773
+ Give suggestions based on what you see in the project. If they don't know yet, suggest \`echo ok\` as a placeholder.
774
+
775
+ **Step 4: Update CLAUDE.md and commit**
776
+
777
+ Rewrite \`CLAUDE.md\` with the project name, description, verify command, and mode: detailed.
778
+
779
+ Update \`runCommand\` and \`mode\` in \`.claude/.tiller.json\`.
780
+
781
+ \`\`\`
782
+ git add CLAUDE.md .claude/.tiller.json
783
+ git commit -m "chore: configure project via /setup"
784
+ \`\`\`
785
+
786
+ Say: "All set. Use /vibe to start working."
787
+ `;
788
+ }
789
+
790
+ // src/scaffold/skills/tech-debt.ts
791
+ function generateTechDebtSkill(config) {
792
+ return `---
793
+ name: tech-debt
794
+ description: Internal skill \u2014 spawned by /vibe to fix one small tech debt item. Not user-invocable directly.
795
+ ---
796
+
797
+ # Tech Debt Agent
798
+
799
+ You are a focused tech debt cleanup agent. Your job: find and fix **one small, non-invasive tech debt item**, merge it to main, then exit cleanly.
800
+
801
+ ## What to look for
802
+
803
+ Scan the codebase for ONE item from these categories (pick the smallest/safest):
804
+
805
+ - **Clutter:** dead code, unused imports/exports, stale TODOs, commented-out code
806
+ - **Duplication:** duplicated logic extractable into a shared helper
807
+ - **Readability:** functions with high cognitive complexity, unclear names, missing comments on non-obvious logic
808
+ - **Correctness risks:** unhandled promise rejections, missing \`await\`, unsafe type assertions (\`as X\`), hardcoded values that should be constants
809
+ - **Inconsistency:** patterns done differently in one place vs. the rest of the codebase
810
+ - **Test health:** happy-path-only tests missing obvious error cases
811
+ - **Dependency hygiene:** packages imported but barely used (could be inlined or removed)
812
+
813
+ ## Guardrails \u2014 you MUST NOT
814
+
815
+ - Touch more than 3 files
816
+ - Change any public API or exported interface
817
+ - Refactor anything that changes observable behavior
818
+ - Split, merge, or move files (structural changes)
819
+ - Rename anything used across many files
820
+ - Modify CI/CD, build config, or dependency versions
821
+ - If nothing safe is found, skip entirely and report "codebase is clean"
822
+
823
+ ## Steps
824
+
825
+ 1. Explore the codebase and identify the single smallest, most non-invasive item to fix
826
+ 2. If nothing safe is found: skip to the "Report" step with "codebase is clean"
827
+ 3. Note the current branch: \`git branch --show-current\`
828
+ 4. Stash any uncommitted work: \`git stash\`
829
+ 5. \`git checkout main\`
830
+ 6. Create a chore branch: \`git checkout -b chore/tech-debt-<short-desc>\` (use kebab-case, max 4 words)
831
+ 7. Fix the item
832
+ 8. Run \`${config.runCommand}\` \u2014 if it fails, revert the change and skip (report "skipped \u2014 verify failed")
833
+ 9. \`git add -A && git commit -m "chore: tech debt \u2014 <short-desc>"\`
834
+ 10. \`git checkout main && git merge --no-ff chore/tech-debt-<short-desc> -m "chore: tech debt \u2014 <short-desc>"\`
835
+ 11. \`git branch -d chore/tech-debt-<short-desc>\`
836
+ 12. \`git checkout <original-branch> && git stash pop\` (restore original state)
837
+ 13. Update \`.claude/.tiller-tech-debt.json\`: set \`lastTechDebtAtFeature\` to the current count of landed features (lines matching \`- [.*] landed feature/\` in \`changelog.md\`)
838
+
839
+ ## Report
840
+
841
+ **simple mode:** "Cleaned up a bit."
842
+ **detailed mode:** "Tech debt fixed: <what was done and why it mattered>"
843
+
844
+ If skipped: **simple:** say nothing. **detailed:** "Tech debt check: codebase is clean."
845
+ `;
846
+ }
847
+
848
+ // src/scaffold/tech-debt-state.ts
849
+ function generateTechDebtState() {
850
+ return JSON.stringify(
851
+ {
852
+ lastTechDebtAtFeature: 0,
853
+ threshold: 3
854
+ },
855
+ null,
856
+ 2
857
+ );
858
+ }
859
+
860
+ // src/scaffold/index.ts
861
+ var TILLER_VERSION = "0.1.0";
862
+ async function scaffold(config, targetDir) {
863
+ const p = (rel) => join2(targetDir, rel);
864
+ await writeFile(p("CLAUDE.md"), generateRootClaudeMd(config));
865
+ await writeFile(p(".gitignore"), generateGitignore(config));
866
+ await writeFile(p("changelog.md"), generateChangelog(config));
867
+ await writeFile(p("vibestate.md"), generateVibestate(config));
868
+ await writeFile(p(".claude/CLAUDE.md"), generateDotClaudeMd(config));
869
+ await writeFile(p(".claude/settings.json"), generateSettingsJson(config));
870
+ await writeFile(p(".claude/.tiller.json"), generateTillerManifest(config, TILLER_VERSION));
871
+ await writeFile(p(".claude/.tiller-tech-debt.json"), generateTechDebtState());
872
+ await writeFile(p(".claude/hooks/post-write.sh"), generatePostWriteHook(config));
873
+ await writeFile(p(".claude/hooks/secret-scan.sh"), generateSecretScanHook(config));
874
+ await writeFile(p(".claude/hooks/session-resume.sh"), generateSessionResumeHook(config));
875
+ await writeFile(p(".claude/skills/setup/SKILL.md"), generateSetupSkill(config));
876
+ await writeFile(p(".claude/skills/vibe/SKILL.md"), generateVibeSkill(config));
877
+ await writeFile(p(".claude/skills/snapshot/SKILL.md"), generateSnapshotSkill(config));
878
+ await writeFile(p(".claude/skills/recap/SKILL.md"), generateRecapSkill(config));
879
+ await writeFile(p(".claude/skills/land/SKILL.md"), generateLandSkill(config));
880
+ await writeFile(p(".claude/skills/tech-debt/SKILL.md"), generateTechDebtSkill(config));
881
+ if (!isGitRepo(targetDir)) {
882
+ gitInit(targetDir);
883
+ }
884
+ gitAdd(targetDir);
885
+ gitCommit(targetDir, "chore: initial tiller scaffold");
886
+ }
887
+
888
+ // src/commands/init.ts
889
+ async function initCommand() {
890
+ const targetDir = resolve(process.cwd());
891
+ const projectName = basename(targetDir);
892
+ intro("tiller-ai init");
893
+ const config = {
894
+ projectName,
895
+ description: "",
896
+ runCommand: "",
897
+ mode: "simple",
898
+ workflow: "solo"
899
+ };
900
+ const s = spinner();
901
+ s.start("Scaffolding...");
902
+ try {
903
+ await scaffold(config, targetDir);
904
+ s.stop("Done.");
905
+ } catch (err) {
906
+ s.stop("Failed.");
907
+ throw err;
908
+ }
909
+ outro(
910
+ `Scaffolded in ./${projectName}
911
+
912
+ claude
913
+
914
+ Then run /setup to configure the project with AI assistance.`
915
+ );
916
+ }
917
+
918
+ // src/commands/upgrade.ts
919
+ import { intro as intro2, outro as outro2, confirm, spinner as spinner2, isCancel, cancel } from "@clack/prompts";
920
+ import { readFile } from "fs/promises";
921
+ import { existsSync as existsSync2 } from "fs";
922
+ import { resolve as resolve2 } from "path";
923
+ var TILLER_VERSION2 = "0.1.0";
924
+ async function upgradeCommand() {
925
+ intro2("tiller-ai upgrade \u2014 update hooks and skills");
926
+ const manifestPath = resolve2(process.cwd(), ".claude/.tiller.json");
927
+ if (!existsSync2(manifestPath)) {
928
+ cancel("No .claude/.tiller.json found. Is this a Tiller project? Run tiller-ai init to start a new project.");
929
+ process.exit(1);
930
+ }
931
+ let manifest;
932
+ try {
933
+ const raw = await readFile(manifestPath, "utf-8");
934
+ manifest = JSON.parse(raw);
935
+ } catch {
936
+ cancel("Failed to read .claude/.tiller.json. The file may be corrupted.");
937
+ process.exit(1);
938
+ }
939
+ const go = await confirm({
940
+ message: `Upgrade Tiller files from v${manifest.version} to v${TILLER_VERSION2}? (managed files will be overwritten)`
941
+ });
942
+ if (isCancel(go) || !go) {
943
+ cancel("Upgrade cancelled.");
944
+ process.exit(0);
945
+ }
946
+ const config = {
947
+ projectName: "",
948
+ description: "",
949
+ runCommand: manifest.runCommand,
950
+ mode: manifest.mode,
951
+ workflow: manifest.workflow ?? "solo"
952
+ };
953
+ const s = spinner2();
954
+ s.start("Upgrading...");
955
+ try {
956
+ await writeFile(".claude/CLAUDE.md", generateDotClaudeMd(config));
957
+ await writeFile(".claude/hooks/post-write.sh", generatePostWriteHook(config));
958
+ await writeFile(".claude/hooks/secret-scan.sh", generateSecretScanHook(config));
959
+ await writeFile(".claude/skills/setup/SKILL.md", generateSetupSkill(config));
960
+ await writeFile(".claude/skills/vibe/SKILL.md", generateVibeSkill(config));
961
+ await writeFile(".claude/skills/snapshot/SKILL.md", generateSnapshotSkill(config));
962
+ await writeFile(".claude/skills/recap/SKILL.md", generateRecapSkill(config));
963
+ await writeFile(".claude/skills/land/SKILL.md", generateLandSkill(config));
964
+ await writeFile(".claude/skills/tech-debt/SKILL.md", generateTechDebtSkill(config));
965
+ await writeFile(".claude/.tiller.json", generateTillerManifest(config, TILLER_VERSION2));
966
+ s.stop("Done!");
967
+ } catch (err) {
968
+ s.stop("Failed.");
969
+ throw err;
970
+ }
971
+ outro2(`Upgraded to v${TILLER_VERSION2}. Managed files: ${MANAGED_FILES.length}`);
972
+ }
973
+
974
+ // src/commands/mode.ts
975
+ import { intro as intro3, outro as outro3, spinner as spinner3, cancel as cancel2 } from "@clack/prompts";
976
+ import { readFile as readFile2, writeFile as fsWriteFile2 } from "fs/promises";
977
+ import { existsSync as existsSync3 } from "fs";
978
+ import { resolve as resolve3 } from "path";
979
+ var TILLER_VERSION3 = "0.1.0";
980
+ async function modeCommand(newMode, options) {
981
+ intro3("tiller-ai mode \u2014 switch between simple and detailed");
982
+ if (newMode !== "simple" && newMode !== "detailed") {
983
+ cancel2('Mode must be "simple" or "detailed".');
984
+ process.exit(1);
985
+ }
986
+ const manifestPath = resolve3(process.cwd(), ".claude/.tiller.json");
987
+ if (!existsSync3(manifestPath)) {
988
+ cancel2("No .claude/.tiller.json found. Is this a Tiller project?");
989
+ process.exit(1);
990
+ }
991
+ let manifest;
992
+ try {
993
+ const raw = await readFile2(manifestPath, "utf-8");
994
+ manifest = JSON.parse(raw);
995
+ } catch {
996
+ cancel2("Failed to read .claude/.tiller.json.");
997
+ process.exit(1);
998
+ }
999
+ const localPath = resolve3(process.cwd(), ".tiller.local.json");
1000
+ const isProjectScope = options.project === true;
1001
+ if (!isProjectScope) {
1002
+ let localMode;
1003
+ if (existsSync3(localPath)) {
1004
+ try {
1005
+ const raw = await readFile2(localPath, "utf-8");
1006
+ const local = JSON.parse(raw);
1007
+ localMode = local.mode;
1008
+ } catch {
1009
+ }
1010
+ }
1011
+ if (localMode === newMode) {
1012
+ outro3(`Already in ${newMode} mode (local override).`);
1013
+ return;
1014
+ }
1015
+ } else {
1016
+ if (manifest.mode === newMode) {
1017
+ outro3(`Already in ${newMode} mode (project default).`);
1018
+ return;
1019
+ }
1020
+ }
1021
+ const rootClaudePath = resolve3(process.cwd(), "CLAUDE.md");
1022
+ let projectName = manifest.projectName ?? "";
1023
+ let description = manifest.description ?? "";
1024
+ if (existsSync3(rootClaudePath)) {
1025
+ try {
1026
+ const raw = await readFile2(rootClaudePath, "utf-8");
1027
+ const nameMatch = raw.match(/^# (.+)$/m);
1028
+ const descMatch = raw.match(/^# .+\n\n(.+)/m);
1029
+ if (nameMatch) projectName = nameMatch[1];
1030
+ if (descMatch) description = descMatch[1];
1031
+ } catch {
1032
+ }
1033
+ }
1034
+ const s = spinner3();
1035
+ if (isProjectScope) {
1036
+ s.start(`Switching project default to ${newMode} mode...`);
1037
+ const config = {
1038
+ projectName,
1039
+ description,
1040
+ runCommand: manifest.runCommand,
1041
+ mode: newMode,
1042
+ workflow: manifest.workflow ?? "solo"
1043
+ };
1044
+ try {
1045
+ await writeFile("CLAUDE.md", generateRootClaudeMd(config));
1046
+ await writeFile(".claude/.tiller.json", generateTillerManifest(config, TILLER_VERSION3));
1047
+ s.stop("Done!");
1048
+ } catch (err) {
1049
+ s.stop("Failed.");
1050
+ throw err;
1051
+ }
1052
+ outro3(`Project default mode set to ${newMode}. Commit CLAUDE.md and .tiller.json to share with the team.`);
1053
+ } else {
1054
+ s.start(`Setting personal mode to ${newMode}...`);
1055
+ let existing = {};
1056
+ if (existsSync3(localPath)) {
1057
+ try {
1058
+ const raw = await readFile2(localPath, "utf-8");
1059
+ existing = JSON.parse(raw);
1060
+ } catch {
1061
+ }
1062
+ }
1063
+ const local = { ...existing, mode: newMode };
1064
+ try {
1065
+ await fsWriteFile2(localPath, JSON.stringify(local, null, 2), "utf-8");
1066
+ s.stop("Done!");
1067
+ } catch (err) {
1068
+ s.stop("Failed.");
1069
+ throw err;
1070
+ }
1071
+ outro3(`Personal mode set to ${newMode} in .tiller.local.json (gitignored). Use --project to change the shared default.`);
1072
+ }
1073
+ }
1074
+
1075
+ // src/commands/workflow.ts
1076
+ import { intro as intro4, outro as outro4, spinner as spinner4, cancel as cancel3 } from "@clack/prompts";
1077
+ import { readFile as readFile3, writeFile as fsWriteFile3 } from "fs/promises";
1078
+ import { existsSync as existsSync4 } from "fs";
1079
+ import { resolve as resolve4 } from "path";
1080
+ var TILLER_VERSION4 = "0.1.0";
1081
+ async function workflowCommand(newWorkflow, options) {
1082
+ intro4("tiller-ai workflow \u2014 switch between solo and team");
1083
+ if (newWorkflow !== "solo" && newWorkflow !== "team") {
1084
+ cancel3('Workflow must be "solo" or "team".');
1085
+ process.exit(1);
1086
+ }
1087
+ const manifestPath = resolve4(process.cwd(), ".claude/.tiller.json");
1088
+ if (!existsSync4(manifestPath)) {
1089
+ cancel3("No .claude/.tiller.json found. Is this a Tiller project?");
1090
+ process.exit(1);
1091
+ }
1092
+ let manifest;
1093
+ try {
1094
+ const raw = await readFile3(manifestPath, "utf-8");
1095
+ manifest = JSON.parse(raw);
1096
+ } catch {
1097
+ cancel3("Failed to read .claude/.tiller.json.");
1098
+ process.exit(1);
1099
+ }
1100
+ const localPath = resolve4(process.cwd(), ".tiller.local.json");
1101
+ const isProjectScope = options.project === true;
1102
+ if (!isProjectScope) {
1103
+ let localWorkflow;
1104
+ if (existsSync4(localPath)) {
1105
+ try {
1106
+ const raw = await readFile3(localPath, "utf-8");
1107
+ const local = JSON.parse(raw);
1108
+ localWorkflow = local.workflow;
1109
+ } catch {
1110
+ }
1111
+ }
1112
+ if (localWorkflow === newWorkflow) {
1113
+ outro4(`Already on ${newWorkflow} workflow (local override).`);
1114
+ return;
1115
+ }
1116
+ } else {
1117
+ if ((manifest.workflow ?? "solo") === newWorkflow) {
1118
+ outro4(`Already on ${newWorkflow} workflow (project default).`);
1119
+ return;
1120
+ }
1121
+ }
1122
+ const rootClaudePath = resolve4(process.cwd(), "CLAUDE.md");
1123
+ let projectName = manifest.projectName ?? "";
1124
+ let description = manifest.description ?? "";
1125
+ if (existsSync4(rootClaudePath)) {
1126
+ try {
1127
+ const raw = await readFile3(rootClaudePath, "utf-8");
1128
+ const nameMatch = raw.match(/^# (.+)$/m);
1129
+ const descMatch = raw.match(/^# .+\n\n(.+)/m);
1130
+ if (nameMatch) projectName = nameMatch[1];
1131
+ if (descMatch) description = descMatch[1];
1132
+ } catch {
1133
+ }
1134
+ }
1135
+ const s = spinner4();
1136
+ if (isProjectScope) {
1137
+ s.start(`Switching project default to ${newWorkflow} workflow...`);
1138
+ const config = {
1139
+ projectName,
1140
+ description,
1141
+ runCommand: manifest.runCommand,
1142
+ mode: manifest.mode ?? "detailed",
1143
+ workflow: newWorkflow
1144
+ };
1145
+ try {
1146
+ await fsWriteFile3(rootClaudePath, generateRootClaudeMd(config), "utf-8");
1147
+ await fsWriteFile3(manifestPath, generateTillerManifest(config, TILLER_VERSION4), "utf-8");
1148
+ s.stop("Done!");
1149
+ } catch (err) {
1150
+ s.stop("Failed.");
1151
+ throw err;
1152
+ }
1153
+ outro4(`Project default workflow set to ${newWorkflow}. Commit CLAUDE.md and .tiller.json to share with the team.`);
1154
+ } else {
1155
+ s.start(`Setting personal workflow to ${newWorkflow}...`);
1156
+ let existing = {};
1157
+ if (existsSync4(localPath)) {
1158
+ try {
1159
+ const raw = await readFile3(localPath, "utf-8");
1160
+ existing = JSON.parse(raw);
1161
+ } catch {
1162
+ }
1163
+ }
1164
+ const local = { ...existing, workflow: newWorkflow };
1165
+ try {
1166
+ await fsWriteFile3(localPath, JSON.stringify(local, null, 2), "utf-8");
1167
+ s.stop("Done!");
1168
+ } catch (err) {
1169
+ s.stop("Failed.");
1170
+ throw err;
1171
+ }
1172
+ outro4(`Personal workflow set to ${newWorkflow} in .tiller.local.json (gitignored). Use --project to change the shared default.`);
1173
+ }
1174
+ }
1175
+
1176
+ // src/index.ts
1177
+ var program = new Command();
1178
+ program.name("tiller").description("Scaffold Claude Code projects with a structured vibe loop").version("0.1.0");
1179
+ program.command("init").description("Scaffold Tiller files in the current directory").action(initCommand);
1180
+ program.command("upgrade").description("Update hooks and skills in an existing Tiller project").action(upgradeCommand);
1181
+ program.command("mode").description("Switch between simple and detailed mode").argument("<mode>", "Mode: simple or detailed").option("--project", "Set the shared project default instead of your personal override").action(modeCommand);
1182
+ program.command("workflow").description("Switch between solo and team workflow").argument("<workflow>", "Workflow: solo or team").option("--project", "Set the shared project default instead of your personal override").action(workflowCommand);
1183
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "tiller-ai",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold Claude Code projects with a structured vibe loop",
5
+ "type": "module",
6
+ "bin": {
7
+ "tiller-ai": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup",
15
+ "dev": "tsup --watch",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "regen": "tsx scripts/regen-skills.ts"
19
+ },
20
+ "dependencies": {
21
+ "@clack/prompts": "^0.9.1",
22
+ "commander": "^12.1.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.0.0",
26
+ "tsup": "^8.3.0",
27
+ "tsx": "^4.21.0",
28
+ "typescript": "^5.6.0",
29
+ "vitest": "^2.1.0"
30
+ },
31
+ "engines": {
32
+ "node": ">=22.0.0"
33
+ },
34
+ "keywords": [
35
+ "claude",
36
+ "claude-code",
37
+ "ai",
38
+ "ai-workflow",
39
+ "scaffold",
40
+ "cli",
41
+ "vibe-coding",
42
+ "developer-tools",
43
+ "code-generation",
44
+ "project-scaffold"
45
+ ],
46
+ "license": "MIT"
47
+ }