Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ jobs:
- name: init-project.sh non-interactive smoke
run: bash tests/init-project-smoke.sh

- name: init-project.sh --demo smoke
run: bash tests/demo-mode-test.sh

- name: create-zachflow wrapper syntax check
run: node --check packages/create-zachflow/index.js

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [Unreleased]

### Added
- **`scripts/init-project.sh --demo` for try-it-out evaluation (M-4).** Synthesizes a throwaway Node.js source repo in a temp directory (3-file scratch — `src/index.js`, `src/index.test.js`, `package.json` — under `templates/demo-source/`), runs `git init` + one commit, then delegates to the existing `--non-interactive` path with an auto-generated `init.config.yaml` wiring a single `backend` role at the throwaway path. Result: zero prompts, fully-configured `sprint-config.yaml` + filled `be-engineer.md` + initialized KB + workflow symlinks, ready for `/sprint demo-1` in Claude Code. Closes the "can't evaluate zachflow without first having a target codebase" friction surfaced in the install/setup UX review. `--demo` is mutually exclusive with `--from=<file>`. The closing banner names the throwaway path so the user can `rm -rf` it when finished. New `tests/demo-mode-test.sh` covers the happy path, source-repo materialization (1 git commit, role.source wired correctly), cleanup-banner content, and the flag-conflict guard; wired into CI.
- **Prerequisite preflight in `scripts/init-project.sh` (H-1).** New `scripts/lib/preflight.sh` is sourced at wizard entry. It checks for `git`, `python3 ≥ 3.8`, the `pyyaml` Python module, and warns on stale `node` (< 18) and unusual `bash` (< 4 outside macOS). Missing items are reported in a single batch with platform-aware install hints — macOS gets `pipx` / `brew` / `pip --break-system-packages` (PEP 668-aware), Linux gets `apt` and `pipx`, Windows points at the installers. Exit 1 stops the wizard before any state is written. New `--skip-preflight` flag bypasses the gate as an escape hatch. Closes the "wizard fails mid-flow with `ModuleNotFoundError: yaml`" UX bug for fresh-machine setups identified in the install/setup review. New `tests/preflight-test.sh` covers happy path, simulated missing-pyyaml detection, and the `--skip-preflight` bypass path; wired into CI alongside a `bash -n` gate.

### Changed
Expand Down
10 changes: 10 additions & 0 deletions MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ bash scripts/init-project.sh --from=init.config.yaml --non-interactive

The shape of `init.config.yaml` is documented inline in `templates/init.config.template.yaml`. After running, compare your generated `sprint-config.yaml` against `examples/nextjs-supabase/sprint-config.example.yaml` to sanity-check the wizard output.

### Try it without your own repo (`--demo`)

```bash
bash scripts/init-project.sh --demo
```

Synthesizes a throwaway Node.js source repo in a temp directory (3 files, one git commit), wires a single backend role at it, initializes the KB, and prints the cleanup path. Zero prompts. Useful for evaluating zachflow before pointing it at real code, or for trying the sprint pipeline end-to-end in a workshop / demo. The wizard prints the throwaway path at the end — delete it with `rm -rf` when you're done.

`--demo` is incompatible with `--from=<file>` (the demo synthesizes its own config).

### Prerequisites

Before the wizard runs, `init-project.sh` checks for `git`, `python3 ≥ 3.8`, the `pyyaml` Python module, and (optionally) `node ≥ 18`. Missing items are reported as a batch with platform-aware install hints (macOS PEP 668 paths included). Pass `--skip-preflight` to bypass the check; the wizard will then fail later if something it actually needs is missing, so reach for this only when you know the check itself is wrong about your environment.
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ That's it. `create-zachflow` clones zachflow at the matching tag (`create-zachfl

To pin a specific zachflow tag, pass `--tag=vX.Y.Z` (defaults to the matching CLI version). To track main, pass `--branch=main`.

**Want to try it without wiring up your own codebase?** After `npx create-zachflow`, run:

```bash
bash scripts/init-project.sh --demo
```

Synthesizes a throwaway Node.js source repo + role + KB so you can run `/sprint demo-1` immediately. The wizard prints the cleanup path at the end.

For non-interactive setup (CI), skip the wizard with `--no-init` and feed a config file:

```bash
Expand Down
126 changes: 113 additions & 13 deletions scripts/init-project.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# bash scripts/init-project.sh --from=init.config.yaml --non-interactive
# bash scripts/init-project.sh --force # skip overwrite confirmations
# bash scripts/init-project.sh --skip-preflight # bypass prerequisite check
# bash scripts/init-project.sh --demo # try zachflow with a throwaway sample repo
#
# Outputs:
# sprint-config.yaml (project root)
Expand All @@ -30,13 +31,16 @@ NON_INTERACTIVE=0
FROM_CONFIG=""
FORCE=0
SKIP_PREFLIGHT=0
DEMO=0
DEMO_SOURCE_PATH=""

while [ $# -gt 0 ]; do
case "$1" in
--non-interactive) NON_INTERACTIVE=1; shift ;;
--from=*) FROM_CONFIG="${1#*=}"; shift ;;
--force) FORCE=1; shift ;;
--skip-preflight) SKIP_PREFLIGHT=1; shift ;;
--demo) DEMO=1; shift ;;
-h|--help)
grep -E '^#( |$)' "${BASH_SOURCE[0]}" | sed 's/^# \?//'
exit 0
Expand All @@ -45,6 +49,11 @@ while [ $# -gt 0 ]; do
esac
done

if [ $DEMO -eq 1 ] && [ -n "$FROM_CONFIG" ]; then
echo "Error: --demo is incompatible with --from=<file> (demo synthesizes its own config)" >&2
exit 1
fi

# ─── Sanity check (must run from zachflow project root) ──────────

if [ ! -f "scripts/install-workflows.sh" ] || [ ! -d "workflows" ] || [ ! -d "templates/teammates" ]; then
Expand All @@ -60,6 +69,86 @@ if [ $SKIP_PREFLIGHT -eq 0 ]; then
run_preflight || exit 1
fi

# ─── Demo mode: synthesize a throwaway source repo + config ──────

# Convert an MSYS/Cygwin path (e.g. /tmp/foo) into a mixed-mode Windows path
# (e.g. C:/.../tmp/foo) when running under git-bash. Both shell builtins and
# Windows-native tools (python3, node) accept mixed-mode paths, so this is
# the safest cross-OS shape for paths handed off to non-bash binaries.
_to_native_path() {
case "$(uname -s 2>/dev/null)" in
MINGW*|MSYS*|CYGWIN*)
if command -v cygpath >/dev/null 2>&1; then
cygpath -m "$1"
return
fi
;;
esac
echo "$1"
}

if [ $DEMO -eq 1 ]; then
if [ ! -d "templates/demo-source" ]; then
echo "Error: templates/demo-source/ is missing — cannot run --demo" >&2
exit 1
fi

# 1. Materialize a throwaway source repo so the synthesized role has
# something real to point at. Lives outside the project tree so
# cleanup is obvious to the user.
DEMO_SOURCE_PATH=$(mktemp -d -t zachflow-demo-source-XXXXXX)
DEMO_SOURCE_PATH=$(_to_native_path "$DEMO_SOURCE_PATH")
echo "demo: staging throwaway source repo at $DEMO_SOURCE_PATH"
(cd templates/demo-source && tar -cf - .) | (cd "$DEMO_SOURCE_PATH" && tar -xf -)
(
cd "$DEMO_SOURCE_PATH"
git init -b main >/dev/null
# Author identity for this throwaway repo — matches create-zachflow's
# fallback so behavior is consistent even on a fresh machine with no
# global git config.
if ! git config --get user.email >/dev/null 2>&1; then
git config user.email "demo@zachflow.local"
git config user.name "zachflow demo"
fi
git add . >/dev/null
git commit -m "chore: demo source seed" >/dev/null
)

# 2. Synthesize a non-interactive config and let the existing
# --non-interactive path do the rest. Single code path, no
# duplicated wizard logic.
DEMO_CONFIG=$(mktemp -t zachflow-demo-config-XXXXXX)
DEMO_CONFIG=$(_to_native_path "$DEMO_CONFIG")
cat > "$DEMO_CONFIG" <<EOF
project_name: zachflow-demo
workflows: both
branch_prefix: demo
roles:
- key: backend
source: $DEMO_SOURCE_PATH
base: main
mode: worktree
teammate: be-engineer
fill:
stack_description: |
Node.js demo source — a 3-file scratch repo (src/index.js, src/index.test.js, package.json) seeded by zachflow's --demo mode.
repo_layout: |
src/ - greet() module + test
package.json
build_cmd: |
npm run build
npm test
conventions: |
- This is a demo scratch project. Anything goes — feel free to break things.
kb:
mode: embedded
init_kb: true
EOF
NON_INTERACTIVE=1
FROM_CONFIG="$DEMO_CONFIG"
FORCE=1 # demo always overwrites — there's no user customization to protect
fi

# ─── Helpers ─────────────────────────────────────────────────────

prompt() {
Expand Down Expand Up @@ -523,17 +612,28 @@ echo
echo "─────────────────────────"
echo "zachflow init complete."
echo
echo "Next:"
case "$WORKFLOWS" in
sprint|both)
echo " /sprint <run-id> # start a sprint"
;;
esac
case "$WORKFLOWS" in
qa-fix|both)
echo " /qa-fix <run-id> --jql=\"...\" # run QA fix loop"
;;
esac
echo
echo "Edit teammate guides at .claude/teammates/<name>.md to refine stack details."
if [ $DEMO -eq 1 ]; then
echo "Demo mode:"
echo " • Source repo: $DEMO_SOURCE_PATH (throwaway — delete when done)"
echo " • Open this directory in Claude Code, then try:"
echo " /sprint demo-1"
echo
echo " Cleanup when finished:"
echo " rm -rf \"$DEMO_SOURCE_PATH\""
echo
else
echo "Next:"
case "$WORKFLOWS" in
sprint|both)
echo " /sprint <run-id> # start a sprint"
;;
esac
case "$WORKFLOWS" in
qa-fix|both)
echo " /qa-fix <run-id> --jql=\"...\" # run QA fix loop"
;;
esac
echo
echo "Edit teammate guides at .claude/teammates/<name>.md to refine stack details."
fi
echo "─────────────────────────"
9 changes: 9 additions & 0 deletions templates/demo-source/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# zachflow demo source

A throwaway sample repo for trying out zachflow without wiring up your own codebase.

`scripts/init-project.sh --demo` copies this directory to a temp location,
runs `git init`, and points a single backend role at it. You can then run
`/sprint demo-1` in Claude Code to walk through a full sprint pipeline
against this scratch project. Delete the temp directory when you're done —
zachflow prints its path at the end of the wizard.
10 changes: 10 additions & 0 deletions templates/demo-source/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "zachflow-demo-source",
"version": "0.0.1",
"private": true,
"description": "Throwaway sample repo used by zachflow's --demo mode.",
"scripts": {
"build": "echo 'demo build OK'",
"test": "node src/index.test.js"
}
}
9 changes: 9 additions & 0 deletions templates/demo-source/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function greet(name) {
return `hello, ${name}`;
}

module.exports = { greet };

if (require.main === module) {
console.log(greet('zachflow'));
}
5 changes: 5 additions & 0 deletions templates/demo-source/src/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const assert = require('node:assert');
const { greet } = require('./index');

assert.strictEqual(greet('world'), 'hello, world');
console.log('demo test OK');
109 changes: 109 additions & 0 deletions tests/demo-mode-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# demo-mode-test.sh — verifies `init-project.sh --demo` produces a working
# zachflow project against a synthesized throwaway source repo.
#
# Asserts:
# 1. --demo completes without user input
# 2. The throwaway source repo is materialized and has its initial commit
# 3. sprint-config.yaml points its single role at the throwaway path
# 4. The final cleanup banner names the throwaway path so the user can
# delete it
# 5. --demo + --from=<file> is a hard error (incompatible flags)

set -euo pipefail

PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
echo "demo-mode test at: $PROJECT_ROOT"

# git-bash returns MSYS paths from mktemp (/tmp/...), which Windows-native
# python3 cannot resolve. Convert to mixed-mode (C:/.../tmp/...) so both
# shell and native binaries accept the same string.
_to_native_path() {
case "$(uname -s 2>/dev/null)" in
MINGW*|MSYS*|CYGWIN*)
command -v cygpath >/dev/null 2>&1 && cygpath -m "$1" || echo "$1"
;;
*) echo "$1" ;;
esac
}

TMPDIR=$(mktemp -d -t zachflow-demo-test-XXXXXX)
TMPDIR=$(_to_native_path "$TMPDIR")
DEMO_SOURCE_PATHS=()
cleanup() {
rm -rf "$TMPDIR"
for p in "${DEMO_SOURCE_PATHS[@]}"; do
[ -n "$p" ] && rm -rf "$p"
done
}
trap cleanup EXIT

# Stage a project copy so we don't pollute the working tree.
STAGE="$TMPDIR/stage"
mkdir -p "$STAGE"
(cd "$PROJECT_ROOT" && tar \
--exclude='./.git' \
--exclude='./.zachflow' \
--exclude='./node_modules' \
--exclude='./.claude/skills/sprint' \
--exclude='./.claude/skills/qa-fix' \
-cf - .) | (cd "$STAGE" && tar -xf -)

# ─── Case 1: --demo completes and produces artifacts ────────────────

echo " [1/3] --demo runs end-to-end without input"
out=$(cd "$STAGE" && bash scripts/init-project.sh --demo 2>&1 </dev/null) || {
echo "FAIL: --demo exited non-zero"
echo "$out" | tail -30
exit 1
}

[ -f "$STAGE/sprint-config.yaml" ] || { echo "FAIL: sprint-config.yaml not produced"; exit 1; }
[ -f "$STAGE/.claude/teammates/be-engineer.md" ] || { echo "FAIL: teammate fill missing"; exit 1; }
[ -d "$STAGE/.zachflow/kb" ] || { echo "FAIL: KB not initialized"; exit 1; }
[ -L "$STAGE/.claude/skills/sprint" ] || { echo "FAIL: sprint symlink missing"; exit 1; }
echo " OK (config + teammate + KB + symlinks all present)"

# ─── Case 2: throwaway source is a real git repo with one commit ────

echo " [2/3] throwaway source repo is materialized correctly"
demo_path=$(echo "$out" | grep "Source repo:" | sed -E 's/.*Source repo: ([^[:space:]]+).*/\1/')
[ -n "$demo_path" ] || { echo "FAIL: could not extract demo source path from output"; echo "$out" | tail -30; exit 1; }
DEMO_SOURCE_PATHS+=("$demo_path")
[ -d "$demo_path/.git" ] || { echo "FAIL: demo source has no .git ($demo_path)"; exit 1; }
[ -f "$demo_path/package.json" ] || { echo "FAIL: demo source missing package.json"; exit 1; }
commits=$(git -C "$demo_path" rev-list --count HEAD)
[ "$commits" = "1" ] || { echo "FAIL: expected 1 commit in demo source, got $commits"; exit 1; }

# sprint-config role.source must point at the same path.
python3 -c "
import yaml, sys
data = yaml.safe_load(open('$STAGE/sprint-config.yaml'))
src = data['repositories']['backend']['source']
assert src == '$demo_path', f'role.source mismatch: got {src}, expected $demo_path'
assert data['project_name'] == 'zachflow-demo'
assert data['branch_prefix'] == 'demo'
print(' sprint-config wired to throwaway source: OK')
"

# Cleanup banner must name the demo path so the user can delete it.
echo "$out" | grep -q "rm -rf \"$demo_path\"" || {
echo "FAIL: cleanup banner missing or doesn't name the demo path"
echo "$out" | tail -15
exit 1
}
echo " OK (1 commit, cleanup banner names the path)"

# ─── Case 3: --demo + --from is rejected ────────────────────────────

echo " [3/3] --demo + --from=<file> is rejected"
set +e
out3=$(cd "$STAGE" && bash scripts/init-project.sh --demo --from=anything.yaml --non-interactive 2>&1 </dev/null)
rc3=$?
set -e
[ $rc3 -ne 0 ] || { echo "FAIL: --demo + --from should have errored"; exit 1; }
echo "$out3" | grep -q "incompatible" || { echo "FAIL: expected 'incompatible' in error"; echo "$out3"; exit 1; }
echo " OK (rc=$rc3, conflict reported)"

echo
echo "PASS: demo-mode tests"
Loading