vinta-ai-workflows 0.7.0-alpha8 → 0.7.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 (67) hide show
  1. package/CHANGELOG.md +472 -595
  2. package/README.md +33 -19
  3. package/package.json +4 -1
  4. package/schemas/README.md +11 -3
  5. package/schemas/ai-models.v1.schema.json +5 -0
  6. package/schemas/interview-answers.v1.schema.json +122 -0
  7. package/schemas/interview-round.v1.schema.json +346 -0
  8. package/schemas/plan-review.v1.schema.json +342 -0
  9. package/schemas/postmortem.v1.schema.json +848 -3
  10. package/schemas/prs-context-comments.v1.schema.json +1 -1
  11. package/schemas/prs-context-frontmatter.v1.schema.json +18 -7
  12. package/schemas/vinta-ai-workflows-config.v1.schema.json +677 -18
  13. package/schemas/workflow.v1.schema.json +751 -26
  14. package/skills/vinta-analyze-codebase/SKILL.md +5 -0
  15. package/skills/vinta-bootstrap-ai-tools/SKILL.md +154 -31
  16. package/skills/vinta-bootstrap-ai-tools/resources/stacks/README.md +1 -1
  17. package/skills/vinta-bootstrap-ai-tools/resources/stacks/django/notes.md +36 -1
  18. package/skills/vinta-bootstrap-ai-tools/resources/worktree-command/README.md +85 -0
  19. package/skills/vinta-bootstrap-ai-tools/resources/worktree-command/lib-template.sh +160 -0
  20. package/skills/vinta-bootstrap-ai-tools/resources/worktree-command/prepare-template.sh +250 -0
  21. package/skills/vinta-bootstrap-ai-tools/resources/worktree-command/teardown-template.sh +96 -0
  22. package/skills/vinta-derive-skills/SKILL.md +23 -15
  23. package/skills/vinta-derive-skills/resources/dependency-license-block.md +8 -9
  24. package/skills/vinta-derive-skills/resources/foundation-skills/add-one-off-script/SKILL.md +1 -1
  25. package/skills/vinta-derive-skills/resources/foundation-skills/create-qa-use-cases/SKILL.md +3 -3
  26. package/skills/vinta-derive-skills/resources/foundation-skills/create-spec/SKILL.md +21 -2
  27. package/skills/vinta-derive-skills/resources/foundation-skills/handoff/SKILL.md +3 -3
  28. package/skills/vinta-derive-skills/resources/foundation-skills/interview-ui/SKILL.md +121 -0
  29. package/skills/vinta-derive-skills/resources/foundation-skills/interview-ui/resources/examples/docs/D4-concurrency.md +28 -0
  30. package/skills/vinta-derive-skills/resources/foundation-skills/interview-ui/resources/examples/round-01.json +60 -0
  31. package/skills/vinta-derive-skills/resources/foundation-skills/open-pr-from-context/SKILL.md +1 -1
  32. package/skills/vinta-derive-skills/resources/foundation-skills/open-pr-from-context/scripts/open-pr.sh +34 -7
  33. package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/SKILL.md +363 -142
  34. package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/resources/ai-models.yaml +19 -13
  35. package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/SKILL.md +39 -21
  36. package/skills/vinta-derive-skills/resources/foundation-skills/thermo-nuclear-code-quality-review/SKILL.md +8 -6
  37. package/skills/vinta-derive-skills/resources/foundation-skills/thermo-nuclear-review-loop/SKILL.md +181 -0
  38. package/skills/vinta-derive-skills/resources/plan-execution/README.md +40 -7
  39. package/skills/vinta-derive-skills/resources/plan-execution/partials/agent-models.md +14 -35
  40. package/skills/vinta-derive-skills/resources/plan-execution/partials/commit-strategy/stacked.md +21 -0
  41. package/skills/vinta-derive-skills/resources/plan-execution/partials/dispatched-agent.md +31 -0
  42. package/skills/vinta-derive-skills/resources/plan-execution/partials/implementer-prompt.md +67 -6
  43. package/skills/vinta-derive-skills/resources/plan-execution/partials/maestro-live-run.md +44 -0
  44. package/skills/vinta-derive-skills/resources/plan-execution/partials/model-pick.md +9 -3
  45. package/skills/vinta-derive-skills/resources/plan-execution/partials/parallel-lanes.md +95 -50
  46. package/skills/vinta-derive-skills/resources/plan-execution/partials/pr-context.md +4 -2
  47. package/skills/vinta-derive-skills/resources/plan-execution/partials/relay-questions.md +13 -0
  48. package/skills/vinta-derive-skills/resources/plan-execution/partials/review-loop.md +81 -0
  49. package/skills/vinta-derive-skills/resources/plan-execution/partials/worktree-seam.md +86 -17
  50. package/skills/vinta-derive-skills/resources/plan-execution/shell/amend-plan-template.md +35 -16
  51. package/skills/vinta-derive-skills/resources/plan-execution/shell/implement-phase-template.md +12 -1
  52. package/skills/vinta-derive-skills/resources/plan-execution/shell/implement-plan-template.md +49 -33
  53. package/skills/vinta-derive-skills/resources/plan-execution/shell/integrate-phase-template.md +2 -1
  54. package/skills/vinta-derive-skills/resources/plan-execution/shell/review-phase-template.md +19 -27
  55. package/skills/vinta-derive-skills/resources/prs-context-template.md +5 -4
  56. package/skills/vinta-derive-skills/resources/systematic-debugging-mcp-tools.md +1 -1
  57. package/skills/vinta-derive-skills/resources/systematic-debugging-template.md +8 -8
  58. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/django.md +4 -1
  59. package/skills/vinta-derive-skills/resources/write-unit-test-template.md +3 -0
  60. package/skills/vinta-derive-subagents/SKILL.md +25 -9
  61. package/skills/vinta-install-ai-tools-setup/SKILL.md +29 -2
  62. package/skills/vinta-migrate-plans-specs/SKILL.md +7 -7
  63. package/skills/vinta-sync-ai-tools/SKILL.md +19 -2
  64. package/skills/vinta-update-project-skills/SKILL.md +1 -0
  65. package/skills/vinta-write-agents-md/SKILL.md +10 -6
  66. package/skills/vinta-write-agents-md/resources/asking-the-human.md +56 -0
  67. package/skills/vinta-derive-skills/resources/plan-execution/partials/review-layers.md +0 -101
@@ -0,0 +1,85 @@
1
+ # Generating the project's worktree command
2
+
3
+ How [vinta-bootstrap-ai-tools](../../SKILL.md) writes a `prepare.sh` / `teardown.sh` pair into a target project. Once written, the pair is the project's `commands.worktree_prepare` / `commands.worktree_teardown`, and `implement-plan` runs it instead of the `prepare-worktree` skill. It runs only when the user answered **`Yes — generate a provisioning script for this project`** to the bootstrap's `prepare-worktree` question.
4
+
5
+ The point is that provisioning becomes deterministic. The decisions the `prepare-worktree` skill makes on every run (what to copy, what to fork, how to isolate compose) are made **once**, here, with the user, and frozen into a script the team owns, reviews and edits.
6
+
7
+ ## What ships
8
+
9
+ | Template | Becomes | Contents |
10
+ |---|---|---|
11
+ | [lib-template.sh](lib-template.sh) | `<dir>/lib.sh` | Resolves the worktree from the `VINTA_WORKTREE_*` environment (or a name given on the command line), plus helpers: `run` (honors `--dry-run`), `clone_dir`, `copy_file`, `set_env`, `forked_db_name`, `require_local_url`, `require_forked_name`, and YAML quoting. |
12
+ | [prepare-template.sh](prepare-template.sh) | `<dir>/prepare.sh` | Sanity checks, `git worktree add`, the **PROJECT STEPS** regions, generic compose isolation, the summary YAML, and the report. On failure it removes the half-made worktree. |
13
+ | [teardown-template.sh](teardown-template.sh) | `<dir>/teardown.sh` | Refuses a dirty worktree without `--force`, runs the **PROJECT STEPS** regions in reverse, stops compose (never `down -v`), removes the worktree, its branch when that branch holds no commits of its own, and the summary. |
14
+ | [gen-compose-worktree-override.sh](../../../vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/gen-compose-worktree-override.sh) | `<dir>/gen-compose-worktree-override.sh` | Copied **only when the project uses docker compose**. `prepare.sh` calls it to fork leaky volumes and strip fixed host ports. |
15
+
16
+ Everything outside the PROJECT STEPS regions is contract plumbing. It is tested in this repo (`tests/worktree-command.test.mjs`), so leave it alone. The regions are where the project lives.
17
+
18
+ ## Steps
19
+
20
+ ### 1. Location
21
+
22
+ The directory was chosen in the bootstrap interview (the `prepare-worktree` generate follow-up) and is already in `.vinta-ai-workflows.yaml` as the parent of `commands.worktree_prepare`. When this step runs standalone and that key is unset, ask now (header `Script dir`): `scripts/worktree (Recommended)`, `bin/worktree`, plus any existing scripts directory the inventory found. Free text covers other paths.
23
+
24
+ **Read before write.** If `prepare.sh`, `teardown.sh` or `lib.sh` already exists there, show the first lines of each and ask (header `Overwrite?`): `Pick another directory (Recommended)`, `Overwrite them`, `Stop — keep my scripts and use them as the command`. The last option records the existing files as `commands.worktree_prepare` / `commands.worktree_teardown` and skips generation.
25
+
26
+ ### 2. Render
27
+
28
+ Copy the three templates (and the compose generator, when compose is in use) and substitute:
29
+
30
+ | Placeholder | Value |
31
+ |---|---|
32
+ | `{{WORKTREE_ROOT}}` | `skills.prepare-worktree.worktree_root` (`.claude/worktrees` or `../<repo>-wt-`) |
33
+ | `{{DEFAULT_BRANCH}}` | `project.default_branch` |
34
+ | `{{SUMMARY_DIR}}` | `skills.prepare-worktree.summary_dir` (default `.vinta-ai-workflows/worktrees`) |
35
+ | `{{PROJECT_NAME}}` | `project.name` |
36
+ | `{{SCRIPT_DIR}}` | the chosen directory, repo-relative |
37
+
38
+ Then `chmod +x` all of them. No `{{…}}` may survive in the written files.
39
+
40
+ ### 3. Fill the PROJECT STEPS regions
41
+
42
+ Each region in `prepare.sh` sits between `# >>> <region>` and `# <<< <region>`, with commented examples. Replace the examples with this project's real commands, and set the state variables at the top of `prepare.sh` that the summary reads. Then write the **matching** inverse region in `teardown.sh`. A prepare step whose teardown is missing leaks state on every lane recycle.
43
+
44
+ The decisions are the same ones the `prepare-worktree` skill makes. Read [its body](../../../vinta-derive-skills/resources/foundation-skills/prepare-worktree/SKILL.md) for the reasoning; the short version for each region:
45
+
46
+ | Region | Decide from | Rules that stay non-negotiable |
47
+ |---|---|---|
48
+ | `deps` | inventory package managers + lockfiles | Every dependency dir is the worktree's own: `clone_dir` it, or reinstall. **Never symlink.** Virtualenvs and yarn PnP always reinstall, because they store absolute paths into main. In a workspace, clone every member's `node_modules`, not only the root's. Set `DEPS_STRATEGY` / `DEPS_PATHS`. |
49
+ | `env` | inventory env model (`.env*`, `.envrc`) | `copy_file`, never symlink, because later regions and compose isolation append to it. Set `ENV_FILES`. Never print an env file's contents. |
50
+ | `dev_db` | inventory DB engine + how it is delivered | Fork with `forked_db_name`, and call `require_local_url` first, every time. A compose-delivered DB is forked by compose isolation (its volume), so the region only points the env at it. Set `DEV_DB_*`, including `DEV_DB_RESET`: a lane with no reset command cannot be reused across a migration. Add compose DB service names to `LOCAL_DB_HOSTS` in `lib.sh`. |
51
+ | `test_db` | inventory test runner | Give the runner its own DB name through the env channel it already reads. Never edit tracked test config. Set `TEST_DB_*` and `TEST_DB_RESET`. |
52
+ | `compose` | a compose file in the repo | `USES_COMPOSE=1`. List in `SHARED_VOLUMES` only read-only caches the user confirms, **never** a data volume. Step 4 of `prepare.sh` does the rest generically. |
53
+ | `other` | Redis / S3 / queues / search / cron / tunnels in the env model | Per-worktree Redis index or key prefix, `S3_PREFIX=wt-$NAME/`, queue and index suffixes, cron off. Run migrations against the forked DB last. |
54
+
55
+ Ask the user only about what the inventory cannot settle, through `AskUserQuestion` with the candidates found: which DBs to fork vs stub, which compose volumes are safe to share, whether cron must stay on. Don't ask about anything the table above already decides.
56
+
57
+ Teardown regions drop only what `prepare.sh` created: call `require_forked_name` before every `dropdb` / `DROP DATABASE`, and make every step tolerate state that is already gone.
58
+
59
+ ### 4. Verify
60
+
61
+ 1. `bash -n` on all three files. Also run `shellcheck` when it is installed.
62
+ 2. Dry run, and show the user the output:
63
+
64
+ ```bash
65
+ <dir>/prepare.sh vinta-smoke --dry-run
66
+ ```
67
+
68
+ 3. Ask (header `Smoke test`): `Dry run is enough (Recommended)`, `Run a real smoke test now`, `Skip`. The real smoke test creates a worktree named `vinta-smoke-<yyyymmdd>`, really forking the local databases the script forks. It then runs the project's lint command inside the worktree and tears it down. When it runs:
69
+ - `prepare.sh` exits 0, and the three conductor checks from the `commands.worktree_prepare` contract hold: the branch, the registered worktree, and main's `git status` unchanged.
70
+ - The summary YAML parses.
71
+ - `teardown.sh` exits 0, and `git worktree list` no longer shows it.
72
+
73
+ On failure, show the failing step's output, run `teardown.sh` with the same name, and ask (header `Fix`): `Let me fix the region and retry (Recommended)`, `Keep the scripts as they are — I'll fix them later`, `Drop the scripts — use the prepare-worktree skill instead`. The last option deletes the generated files, and flips `foundation_skills.prepare-worktree` to `enabled` and the two `commands.worktree_*` keys off.
74
+
75
+ ### 5. Record
76
+
77
+ Set `commands.worktree_prepare: <dir>/prepare.sh` and `commands.worktree_teardown: <dir>/teardown.sh` in `.vinta-ai-workflows.yaml` (Step 0.5 already wrote them from the chosen directory; confirm they match). Tell the user the scripts are theirs to commit and edit, and that `teardown.sh <name>` is also how a human removes a worktree they made by hand with `prepare.sh <name>`.
78
+
79
+ ## Pitfalls
80
+
81
+ - **Hard-coding the main checkout's path, a branch or a DB name.** Everything per-worktree derives from `$NAME` through `forked_db_name` / `COMPOSE_PROJECT`, so lanes never collide.
82
+ - **Writing outside `$WT_PATH`.** The conductor fails provisioning when main's `git status` changes. The only out-of-tree writes allowed are forked databases, compose volumes, and files under the summary dir.
83
+ - **Forgetting `run`.** A bare command executes under `--dry-run` too. Wrap pipes and redirections as `run sh -c '…'`.
84
+ - **A partial summary.** `prepare.sh` writes every key. When you add state, add it to the variables at the top rather than appending keys to the heredoc.
85
+ - **Logging secrets.** Connection strings carry passwords. `require_local_url` never prints the URL; keep it that way in any message you add.
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env bash
2
+ # Shared plumbing for prepare.sh and teardown.sh: resolves the worktree's
3
+ # identity from the VINTA_WORKTREE_* environment (or from a name given on the
4
+ # command line), and provides the small helpers both scripts use. Sourced, never
5
+ # run. Generated by the vinta-ai-workflows bootstrap; owned by this project now.
6
+ #
7
+ # Bash 3.2 compatible (the macOS system bash): no associative arrays, no
8
+ # ${var,,}, no mapfile.
9
+
10
+ DRY_RUN="${VINTA_WORKTREE_DRY_RUN:-0}"
11
+ FORCE="${VINTA_WORKTREE_FORCE:-0}"
12
+ name_arg=""
13
+ for arg in "$@"; do
14
+ case "$arg" in
15
+ --dry-run) DRY_RUN=1 ;;
16
+ --force) FORCE=1 ;;
17
+ -h|--help) sed -n '2,/^set -euo/p' "$0" | sed '$d'; exit 0 ;;
18
+ -*) echo "unknown option: $arg" >&2; exit 2 ;;
19
+ *) name_arg="$arg" ;;
20
+ esac
21
+ done
22
+
23
+ # Resolves symlinks in the deepest part of a path that exists. `git worktree
24
+ # list` prints resolved paths, so every comparison against it needs this (on
25
+ # macOS /var and /tmp are symlinks into /private).
26
+ canon_path() {
27
+ local p="$1" rest=""
28
+ while [ ! -d "$p" ]; do rest="/$(basename "$p")$rest"; p="$(dirname "$p")"; done
29
+ printf '%s%s' "$(cd "$p" && pwd -P)" "$rest"
30
+ }
31
+
32
+ MAIN="$(canon_path "${VINTA_MAIN_CHECKOUT:-$(git rev-parse --show-toplevel)}")"
33
+ NAME="${VINTA_WORKTREE_NAME:-$name_arg}"
34
+ if [ -z "$NAME" ]; then
35
+ echo "usage: $0 <worktree-name> [--dry-run] (or set VINTA_WORKTREE_NAME)" >&2
36
+ exit 2
37
+ fi
38
+ case "$NAME" in
39
+ *[!A-Za-z0-9._-]*) echo "worktree name may only use [A-Za-z0-9._-]: $NAME" >&2; exit 2 ;;
40
+ esac
41
+
42
+ # Project defaults, baked in at generation time. implement-plan always passes
43
+ # the VINTA_WORKTREE_* values explicitly; these only serve a human running the
44
+ # script by hand.
45
+ WORKTREE_ROOT="{{WORKTREE_ROOT}}"
46
+ DEFAULT_BASE_REF="origin/{{DEFAULT_BRANCH}}"
47
+ SUMMARY_DIR="{{SUMMARY_DIR}}"
48
+
49
+ case "$WORKTREE_ROOT" in /*) root="$WORKTREE_ROOT" ;; *) root="$MAIN/$WORKTREE_ROOT" ;; esac
50
+ # A root ending in "-" is the `../<repo>-wt-` sibling convention: no slash.
51
+ case "$root" in *-) default_path="$root$NAME" ;; *) default_path="$root/$NAME" ;; esac
52
+
53
+ WT_PATH="$(canon_path "${VINTA_WORKTREE_PATH:-$default_path}")"
54
+ BRANCH="${VINTA_WORKTREE_BRANCH:-$NAME}"
55
+ BASE_REF="${VINTA_WORKTREE_BASE_REF:-$DEFAULT_BASE_REF}"
56
+ KIND="${VINTA_WORKTREE_KIND:-single}"
57
+ PLAN_PATH="${VINTA_PLAN_PATH:-}"
58
+ SUMMARY="${VINTA_WORKTREE_SUMMARY:-$MAIN/$SUMMARY_DIR/$NAME.yaml}"
59
+
60
+ # Names derived from the worktree name. Every forked database, compose project,
61
+ # volume and cache key comes from these, so prepare.sh and teardown.sh always
62
+ # agree on what belongs to this worktree.
63
+ REPO_SLUG="$(basename "$MAIN" | tr -c 'A-Za-z0-9_\n' '_' | tr 'A-Z' 'a-z')"
64
+ WT_SLUG="$(printf '%s' "$NAME" | tr -c 'A-Za-z0-9_' '_' | tr 'A-Z' 'a-z')"
65
+ COMPOSE_PROJECT="${REPO_SLUG}_wt_${WT_SLUG}"
66
+ COMPOSE_OVERRIDE="$(dirname "$SUMMARY")/$NAME.docker-compose.override.yml"
67
+
68
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
69
+
70
+ log() { printf '[%s %s] %s\n' "$(basename "$0" .sh)" "$NAME" "$*" >&2; }
71
+
72
+ # Runs a command, or prints it under --dry-run. For pipes or redirections, wrap
73
+ # them: run sh -c '...'.
74
+ run() {
75
+ if [ "$DRY_RUN" = 1 ]; then
76
+ printf '+ %s\n' "$*" >&2
77
+ else
78
+ "$@"
79
+ fi
80
+ }
81
+
82
+ # The forked name for one of main's databases, e.g. app_dev -> app_dev_wt_lane_1.
83
+ forked_db_name() { printf '%s_wt_%s' "$1" "$WT_SLUG"; }
84
+
85
+ # Hosts a database URL may point at for this script to fork or drop it. Add the
86
+ # project's compose service names (e.g. "db") when the DB runs in compose.
87
+ LOCAL_DB_HOSTS="localhost 127.0.0.1 ::1"
88
+
89
+ # Refuses anything but a local database. Forking or dropping is only ever done
90
+ # against a developer's own server, never a shared or remote one. Never prints
91
+ # the URL: it can carry a password.
92
+ require_local_url() {
93
+ local rest host allowed
94
+ case "$1" in sqlite:*|file:*) return 0 ;; esac
95
+ rest="${1#*://}"
96
+ rest="${rest##*@}"
97
+ case "$rest" in
98
+ \[*) host="${rest#\[}"; host="${host%%\]*}" ;;
99
+ *) host="${rest%%[:/?]*}" ;;
100
+ esac
101
+ for allowed in $LOCAL_DB_HOSTS; do
102
+ [ "$host" = "$allowed" ] && return 0
103
+ done
104
+ echo "refusing to fork or drop a database on non-local host '$host' (allowed: $LOCAL_DB_HOSTS)" >&2
105
+ exit 1
106
+ }
107
+
108
+ # Refuses to drop a database this worktree did not create.
109
+ require_forked_name() {
110
+ case "$1" in
111
+ *_wt_"$WT_SLUG") return 0 ;;
112
+ esac
113
+ echo "refusing to drop '$1': it is not a '*_wt_$WT_SLUG' database" >&2
114
+ exit 1
115
+ }
116
+
117
+ # Copy-on-write clone of a directory from main into the worktree: APFS clonefile
118
+ # on macOS, reflink on btrfs / XFS, plain copy elsewhere. Links inside the tree
119
+ # stay links, which keeps pnpm's relative-link store valid.
120
+ clone_dir() {
121
+ [ -e "$MAIN/$1" ] || { log "skip $1 (absent in main)"; return 0; }
122
+ run mkdir -p "$(dirname "$WT_PATH/$1")"
123
+ if [ "$(uname)" = Darwin ]; then
124
+ run cp -ac "$MAIN/$1" "$WT_PATH/$1"
125
+ else
126
+ run cp -a --reflink=auto "$MAIN/$1" "$WT_PATH/$1"
127
+ fi
128
+ }
129
+
130
+ copy_file() {
131
+ [ -e "$MAIN/$1" ] || { log "skip $1 (absent in main)"; return 0; }
132
+ run mkdir -p "$(dirname "$WT_PATH/$1")"
133
+ run cp -p "$MAIN/$1" "$WT_PATH/$1"
134
+ }
135
+
136
+ # Sets KEY=VALUE in a worktree env file, replacing an existing line for KEY.
137
+ set_env() {
138
+ local file="$WT_PATH/$1" key="$2" value="$3"
139
+ if [ "$DRY_RUN" = 1 ]; then printf '+ set %s in %s\n' "$key" "$1" >&2; return 0; fi
140
+ touch "$file"
141
+ grep -v "^$key=" "$file" > "$file.tmp" || true
142
+ printf '%s=%s\n' "$key" "$value" >> "$file.tmp"
143
+ mv "$file.tmp" "$file"
144
+ }
145
+
146
+ # YAML scalar: null for empty, single-quoted otherwise.
147
+ yaml_str() {
148
+ if [ -z "$1" ] || [ "$1" = null ]; then
149
+ printf 'null'
150
+ else
151
+ printf "'%s'" "$(printf '%s' "$1" | sed "s/'/''/g")"
152
+ fi
153
+ }
154
+
155
+ # YAML flow list from space-separated words.
156
+ yaml_list() {
157
+ local out="" sep="" item
158
+ for item in $1; do out="$out$sep$(yaml_str "$item")"; sep=", "; done
159
+ printf '[%s]' "$out"
160
+ }
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env bash
2
+ # Provision a runnable git worktree for {{PROJECT_NAME}}.
3
+ #
4
+ # implement-plan runs this as `commands.worktree_prepare`, with the
5
+ # VINTA_WORKTREE_* environment set. You can also run it by hand:
6
+ #
7
+ # {{SCRIPT_DIR}}/prepare.sh <worktree-name> [--dry-run]
8
+ #
9
+ # --dry-run (or VINTA_WORKTREE_DRY_RUN=1) prints every command instead of
10
+ # running it, and writes nothing.
11
+ #
12
+ # What it guarantees (the commands.worktree_prepare contract):
13
+ # - a worktree at $VINTA_WORKTREE_PATH, on the new branch
14
+ # $VINTA_WORKTREE_BRANCH, based on $VINTA_WORKTREE_BASE_REF;
15
+ # - runnable on its own: dependencies, env files, databases and compose
16
+ # project are the worktree's, never shared writable state with main;
17
+ # - a non-zero exit on any failure, and never a write to main's tracked files;
18
+ # - running again for the same name works once teardown.sh has run.
19
+ #
20
+ # Generated by the vinta-ai-workflows bootstrap. This project owns it now: edit
21
+ # the PROJECT STEPS regions freely, and keep teardown.sh in step with them.
22
+ set -euo pipefail
23
+ . "$(cd "$(dirname "$0")" && pwd)/lib.sh" "$@"
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # What this worktree got. The PROJECT STEPS below set these; the summary at the
27
+ # end records them so teardown and implement-plan's lane reset can read them.
28
+ # ---------------------------------------------------------------------------
29
+ DEPS_STRATEGY=copy # copy | reinstall
30
+ DEPS_PATHS="" # space-separated, relative to the worktree
31
+ ENV_STRATEGY=copy # copy | symlink
32
+ ENV_FILES=""
33
+ DEV_DB_ENGINE="" # postgres | mysql | sqlite | mongo | redis; empty = no dev DB
34
+ DEV_DB_STRATEGY=fork # fork | share | stub
35
+ DEV_DB_FORKED=""
36
+ DEV_DB_URL_VAR=""
37
+ DEV_DB_RESET="" # command that returns the dev DB to a fresh state
38
+ TEST_DB_ENGINE="" # empty = no test DB
39
+ TEST_DB_STRATEGY=fork # fork | share
40
+ TEST_DB_FORKED=""
41
+ TEST_DB_URL_VAR=""
42
+ TEST_DB_RESET=""
43
+ USES_COMPOSE=0 # 1 when the project runs services through docker compose
44
+ COMPOSE_NETWORK=per-worktree
45
+ SHARED_VOLUMES="" # compose volume keys safe to share: read-only caches, never data
46
+ REDIS_DB_INDEX=""
47
+ S3_PREFIX=""
48
+ CRON_DISABLED=false
49
+ NOTES=""
50
+
51
+ created_worktree=0
52
+ on_exit() {
53
+ local status=$?
54
+ [ "$status" -eq 0 ] && return 0
55
+ [ "$DRY_RUN" = 1 ] && return 0
56
+ log "provisioning failed (exit $status)"
57
+ if [ "$created_worktree" = 1 ]; then
58
+ log "removing the half-made worktree at $WT_PATH"
59
+ git -C "$MAIN" worktree remove --force "$WT_PATH" >/dev/null 2>&1 || true
60
+ git -C "$MAIN" branch -D "$BRANCH" >/dev/null 2>&1 || true
61
+ fi
62
+ log "databases or volumes created before the failure are removed by teardown.sh, run with the same VINTA_WORKTREE_* environment"
63
+ }
64
+ trap on_exit EXIT
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # 1. Sanity
68
+ # ---------------------------------------------------------------------------
69
+ if git -C "$MAIN" worktree list --porcelain | grep -Fxq "worktree $WT_PATH"; then
70
+ echo "a worktree is already registered at $WT_PATH; run teardown.sh first" >&2
71
+ exit 1
72
+ fi
73
+ if [ -e "$WT_PATH" ]; then
74
+ echo "$WT_PATH already exists and is not a registered worktree; move it aside first" >&2
75
+ exit 1
76
+ fi
77
+ if git -C "$MAIN" show-ref --verify --quiet "refs/heads/$BRANCH"; then
78
+ echo "branch $BRANCH already exists; pick another name or delete it" >&2
79
+ exit 1
80
+ fi
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # 2. Create the worktree
84
+ # ---------------------------------------------------------------------------
85
+ log "creating $WT_PATH on $BRANCH from $BASE_REF"
86
+ case "$BASE_REF" in
87
+ origin/*) run git -C "$MAIN" fetch --quiet origin || log "fetch failed; using the local $BASE_REF" ;;
88
+ esac
89
+ run mkdir -p "$(dirname "$WT_PATH")"
90
+ run git -C "$MAIN" worktree add --quiet -b "$BRANCH" "$WT_PATH" "$BASE_REF"
91
+ created_worktree=1
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # 3. PROJECT STEPS. Each region is filled in for this project at generation
95
+ # time. Write only inside "$WT_PATH", to databases named by
96
+ # `forked_db_name`, or under the summary dir. Use `run` for every command so
97
+ # --dry-run stays honest.
98
+ # ---------------------------------------------------------------------------
99
+
100
+ # >>> deps: every dependency dir is the worktree's own; never a symlink to main.
101
+ # clone_dir node_modules # copy-on-write; also each workspace member's
102
+ # (cd "$WT_PATH" && run uv sync) # virtualenvs always reinstall
103
+ # DEPS_PATHS="node_modules"
104
+ # <<< deps
105
+
106
+ # >>> env: copy env files (compose isolation below appends to .env, so it must
107
+ # be a copy), then point their connection strings at this worktree's state.
108
+ # copy_file .env
109
+ # ENV_FILES=".env"
110
+ # <<< env
111
+
112
+ # >>> dev_db: fork the local dev database (or leave DEV_DB_ENGINE empty).
113
+ # require_local_url "$main_database_url"
114
+ # DEV_DB_FORKED="$(forked_db_name app_dev)"
115
+ # run createdb -T app_dev "$DEV_DB_FORKED"
116
+ # set_env .env DATABASE_URL "postgres://localhost:5432/$DEV_DB_FORKED"
117
+ # DEV_DB_ENGINE=postgres DEV_DB_URL_VAR=DATABASE_URL
118
+ # DEV_DB_RESET="dropdb --if-exists $DEV_DB_FORKED && createdb -T app_dev $DEV_DB_FORKED"
119
+ # <<< dev_db
120
+
121
+ # >>> test_db: give the test runner its own database name.
122
+ # TEST_DB_FORKED="$(forked_db_name app_test)"
123
+ # set_env .env TEST_DATABASE_URL "postgres://localhost:5432/$TEST_DB_FORKED"
124
+ # TEST_DB_ENGINE=postgres TEST_DB_URL_VAR=TEST_DATABASE_URL
125
+ # TEST_DB_RESET="dropdb --if-exists $TEST_DB_FORKED"
126
+ # <<< test_db
127
+
128
+ # >>> compose: set USES_COMPOSE=1 when the project runs services through
129
+ # docker compose. Step 4 then isolates the stack generically.
130
+ # USES_COMPOSE=1
131
+ # SHARED_VOLUMES=""
132
+ # <<< compose
133
+
134
+ # >>> other: Redis index / key prefix, S3 prefix, queue and index suffixes,
135
+ # cron off, migrations against the forked DB.
136
+ # S3_PREFIX="wt-$NAME/"; set_env .env S3_PREFIX "$S3_PREFIX"
137
+ # (cd "$WT_PATH" && run python manage.py migrate)
138
+ # <<< other
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # 4. Compose isolation. COMPOSE_PROJECT_NAME alone does not isolate external or
142
+ # fixed-name volumes, or fixed host ports; the generated out-of-tree override
143
+ # does. Never edit the project's tracked compose files.
144
+ # ---------------------------------------------------------------------------
145
+ FORKED_VOLUMES_YAML="[]"
146
+ PORTS_STRIPPED=""
147
+ BASE_COMPOSE_FILE=""
148
+ if [ "$USES_COMPOSE" = 1 ]; then
149
+ run mkdir -p "$(dirname "$COMPOSE_OVERRIDE")"
150
+ share_args=""
151
+ for vol in $SHARED_VOLUMES; do share_args="$share_args --share-volume $vol"; done
152
+ report="$(mktemp)"
153
+ # shellcheck disable=SC2086 # share_args is a deliberate word list
154
+ run "$SCRIPT_DIR/gen-compose-worktree-override.sh" --main "$MAIN" --worktree "$WT_PATH" \
155
+ --name "$NAME" --out "$COMPOSE_OVERRIDE" $share_args 2> "$report" >/dev/null
156
+ cat "$report" >&2
157
+ for f in compose.yaml compose.yml docker-compose.yaml docker-compose.yml; do
158
+ if [ -f "$MAIN/$f" ]; then BASE_COMPOSE_FILE="$f"; break; fi
159
+ done
160
+ set_env .env COMPOSE_PROJECT_NAME "$COMPOSE_PROJECT"
161
+ set_env .env COMPOSE_FILE "$BASE_COMPOSE_FILE:$COMPOSE_OVERRIDE"
162
+ case " $ENV_FILES " in *" .env "*) ;; *) ENV_FILES="$ENV_FILES .env" ;; esac
163
+ # The generator reports "- <key> (<reason>) -> <volume>" per forked volume.
164
+ FORKED_VOLUMES_YAML="$(sed -n 's/^ - \([^ ]*\) (\(.*\)) -> \(.*\)$/\1|\2|\3/p' "$report" | {
165
+ out="" sep=""
166
+ while IFS='|' read -r key reason pinned; do
167
+ case "$reason" in external*) reason=external ;; *) reason=fixed-name ;; esac
168
+ out="$out$sep{volume_key: $(yaml_str "$key"), reason: $reason, forked_name: $(yaml_str "$pinned")}"
169
+ sep=", "
170
+ done
171
+ printf '[%s]' "$out"
172
+ })"
173
+ PORTS_STRIPPED="$(sed -n 's/^ stripped host-port publishing from: //p' "$report" | sed 's/^none$//; s/,//g')"
174
+ rm -f "$report"
175
+ fi
176
+
177
+ # ---------------------------------------------------------------------------
178
+ # 5. Summary: read by teardown, by implement-plan's lane reset, and by
179
+ # vinta-ai-maestro. Every key is present; null means none.
180
+ # ---------------------------------------------------------------------------
181
+ db_yaml() { # engine strategy forked url_var reset
182
+ if [ -z "$1" ]; then printf 'null'; return 0; fi
183
+ printf '\n engine: %s\n strategy: %s\n forked_name: %s\n connection_url_var: %s\n reset_cmd: %s' \
184
+ "$1" "$2" "$(yaml_str "$3")" "$(yaml_str "$4")" "$(yaml_str "$5")"
185
+ }
186
+
187
+ if [ "$USES_COMPOSE" = 1 ]; then
188
+ compose_yaml="$(printf '\n project_name: %s\n network_strategy: %s\n override_path: %s\n base_compose_file: %s\n forked_volumes: %s\n shared_volumes: %s\n ports_stripped_from: %s' \
189
+ "$(yaml_str "$COMPOSE_PROJECT")" "$COMPOSE_NETWORK" "$(yaml_str "$COMPOSE_OVERRIDE")" \
190
+ "$(yaml_str "$BASE_COMPOSE_FILE")" "$FORKED_VOLUMES_YAML" "$(yaml_list "$SHARED_VOLUMES")" \
191
+ "$(yaml_list "$PORTS_STRIPPED")")"
192
+ else
193
+ compose_yaml=null
194
+ fi
195
+
196
+ if [ "$DRY_RUN" = 1 ]; then
197
+ log "dry run: would write the summary to $SUMMARY"
198
+ else
199
+ mkdir -p "$(dirname "$SUMMARY")"
200
+ cat > "$SUMMARY" <<EOF
201
+ # Written by $(basename "$0"); read by teardown.sh and implement-plan.
202
+ name: $(yaml_str "$NAME")
203
+ path: $(yaml_str "$WT_PATH")
204
+ branch: $(yaml_str "$BRANCH")
205
+ base_ref: $(yaml_str "$BASE_REF")
206
+ created_at: $(yaml_str "$(date -u +%Y-%m-%dT%H:%M:%SZ)")
207
+ plan_path: $(yaml_str "$PLAN_PATH")
208
+ flags:
209
+ deps_change: true
210
+ schema_change: true
211
+ env_change: true
212
+ test_infra_change: true
213
+ compose_change: $([ "$USES_COMPOSE" = 1 ] && echo true || echo false)
214
+ state:
215
+ deps:
216
+ strategy: $DEPS_STRATEGY
217
+ paths: $(yaml_list "$DEPS_PATHS")
218
+ env:
219
+ strategy: $ENV_STRATEGY
220
+ files: $(yaml_list "$ENV_FILES")
221
+ dev_db: $(db_yaml "$DEV_DB_ENGINE" "$DEV_DB_STRATEGY" "$DEV_DB_FORKED" "$DEV_DB_URL_VAR" "$DEV_DB_RESET")
222
+ test_db: $(db_yaml "$TEST_DB_ENGINE" "$TEST_DB_STRATEGY" "$TEST_DB_FORKED" "$TEST_DB_URL_VAR" "$TEST_DB_RESET")
223
+ compose: $compose_yaml
224
+ other:
225
+ redis_db_index: $(yaml_str "$REDIS_DB_INDEX")
226
+ s3_prefix: $(yaml_str "$S3_PREFIX")
227
+ cron_disabled: $CRON_DISABLED
228
+ sandbox:
229
+ tier: none
230
+ launcher: null
231
+ deny: []
232
+ allow: []
233
+ notes: $(yaml_str "Provisioned by $(basename "$0") (kind: $KIND). $NOTES")
234
+ EOF
235
+ fi
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # 6. Report (implement-plan quotes the tail of this)
239
+ # ---------------------------------------------------------------------------
240
+ cat <<EOF
241
+ worktree: $WT_PATH
242
+ branch: $BRANCH (from $BASE_REF)
243
+ deps: $DEPS_STRATEGY ${DEPS_PATHS:-(none)}
244
+ env: $ENV_STRATEGY ${ENV_FILES:-(none)}
245
+ dev db: ${DEV_DB_FORKED:-(none)}
246
+ test db: ${TEST_DB_FORKED:-(none)}
247
+ compose: $([ "$USES_COMPOSE" = 1 ] && echo "$COMPOSE_PROJECT" || echo "(not used)")
248
+ summary: $SUMMARY
249
+ teardown: $SCRIPT_DIR/teardown.sh $NAME
250
+ EOF
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env bash
2
+ # Remove a worktree prepare.sh made for {{PROJECT_NAME}}, and everything it
3
+ # created outside git.
4
+ #
5
+ # implement-plan runs this as `commands.worktree_teardown`, with the same
6
+ # VINTA_WORKTREE_* environment the worktree was provisioned with. You can also
7
+ # run it by hand:
8
+ #
9
+ # {{SCRIPT_DIR}}/teardown.sh <worktree-name> [--dry-run] [--force]
10
+ #
11
+ # Safe to run more than once: every step skips what is already gone. It refuses
12
+ # a worktree with uncommitted changes unless --force (or VINTA_WORKTREE_FORCE=1)
13
+ # is given, and only drops databases named `*_wt_<worktree>`. It never runs
14
+ # `docker compose down -v`, which would also delete volumes shared with main.
15
+ #
16
+ # Generated by the vinta-ai-workflows bootstrap. Keep the PROJECT STEPS regions
17
+ # in step with prepare.sh's.
18
+ set -euo pipefail
19
+ . "$(cd "$(dirname "$0")" && pwd)/lib.sh" "$@"
20
+
21
+ registered=0
22
+ if git -C "$MAIN" worktree list --porcelain | grep -Fxq "worktree $WT_PATH"; then
23
+ registered=1
24
+ fi
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # 1. Refuse to lose work
28
+ # ---------------------------------------------------------------------------
29
+ if [ "$registered" = 1 ] && [ "$FORCE" != 1 ]; then
30
+ if [ -n "$(git -C "$WT_PATH" status --porcelain)" ]; then
31
+ echo "$WT_PATH has uncommitted changes; commit or stash them, or pass --force to discard them" >&2
32
+ exit 1
33
+ fi
34
+ fi
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # 2. PROJECT STEPS, in reverse order of prepare.sh. Each must tolerate state
38
+ # that is already gone.
39
+ # ---------------------------------------------------------------------------
40
+
41
+ # >>> other: undo what prepare.sh's "other" region created outside the worktree.
42
+ # <<< other
43
+
44
+ # >>> test_db: drop the forked test database.
45
+ # db="$(forked_db_name app_test)"; require_forked_name "$db"
46
+ # run dropdb --if-exists "$db"
47
+ # <<< test_db
48
+
49
+ # >>> dev_db: drop the forked dev database.
50
+ # db="$(forked_db_name app_dev)"; require_forked_name "$db"
51
+ # run dropdb --if-exists "$db"
52
+ # <<< dev_db
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # 3. Compose: stop the stack, then remove only the volumes the override pinned
56
+ # for this worktree. Shared volumes stay.
57
+ # ---------------------------------------------------------------------------
58
+ if [ -f "$COMPOSE_OVERRIDE" ]; then
59
+ if [ "$registered" = 1 ] && command -v docker >/dev/null 2>&1; then
60
+ (cd "$WT_PATH" && run docker compose -p "$COMPOSE_PROJECT" down) || log "compose down failed; continuing"
61
+ fi
62
+ # The override's volumes: block lists each pinned name; that is the manifest.
63
+ for vol in $(sed -n '/^volumes:/,/^[^ ]/s/^ name: "\(.*\)"$/\1/p' "$COMPOSE_OVERRIDE"); do
64
+ case "$vol" in
65
+ *_wt_"$NAME"_*) run docker volume rm "$vol" >/dev/null 2>&1 || log "volume $vol already gone" ;;
66
+ *) log "skip volume $vol: not named for this worktree" ;;
67
+ esac
68
+ done
69
+ run rm -f "$COMPOSE_OVERRIDE"
70
+ fi
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # 4. The worktree and its branch
74
+ # ---------------------------------------------------------------------------
75
+ if [ "$registered" = 1 ]; then
76
+ if [ "$FORCE" = 1 ]; then
77
+ run git -C "$MAIN" worktree remove --force "$WT_PATH"
78
+ else
79
+ run git -C "$MAIN" worktree remove "$WT_PATH"
80
+ fi
81
+ else
82
+ log "no worktree registered at $WT_PATH"
83
+ fi
84
+ # The branch goes only when it holds no commits of its own (its tip is already
85
+ # in the base ref, or merged into main's HEAD). Anything else is kept.
86
+ if git -C "$MAIN" show-ref --verify --quiet "refs/heads/$BRANCH"; then
87
+ if git -C "$MAIN" merge-base --is-ancestor "$BRANCH" "$BASE_REF" 2>/dev/null \
88
+ || git -C "$MAIN" merge-base --is-ancestor "$BRANCH" HEAD 2>/dev/null; then
89
+ run git -C "$MAIN" branch -D "$BRANCH" >/dev/null
90
+ else
91
+ log "kept branch $BRANCH: it has commits not in $BASE_REF (delete it with git branch -D once you are sure)"
92
+ fi
93
+ fi
94
+
95
+ run rm -f "$SUMMARY"
96
+ log "done"