diff --git a/amplifier-bundle/recipes/consensus-workflow.yaml b/amplifier-bundle/recipes/consensus-workflow.yaml index cb2f0ecde..ef760d44c 100644 --- a/amplifier-bundle/recipes/consensus-workflow.yaml +++ b/amplifier-bundle/recipes/consensus-workflow.yaml @@ -524,8 +524,26 @@ steps: Commands to execute: ```bash + # NOTE: This pipeline mirrors default-workflow.yaml step-04-setup-worktree with a + # 30-char limit (vs 50). Keep both in sync on security patches. See Issue #2952. + # REQ-SEC-001 (tracked in #2974): single-quote wrapping mitigates word-splitting + # but does not prevent injection via single-quote chars in task_description. + # Full fix requires env-var injection in the recipe runner. # Create branch name - BRANCH_NAME="feat/consensus-$(echo '{{task_description}}' | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | cut -c1-30)" + TASK_DESC=$(printf '%s' '{{task_description}}') + TASK_SLUG=$(printf '%s' "$TASK_DESC" | tr '\n\r' ' ' | tr '[:upper:]' '[:lower:]' | tr -s ' ' '-' | sed 's/[^a-z0-9-]//g' | sed 's/-\{2,\}/-/g' | sed 's/^-//;s/-$//' | cut -c1-30 | sed 's/-$//') + # Guard: empty slug (blank/all-symbol input) produces a trailing-hyphen branch that + # git check-ref-format accepts as valid but is undesirable. Catch it explicitly first. + if [ -z "$TASK_SLUG" ]; then + echo "WARNING: task_description produced an empty slug — falling back to task-unnamed" >&2 + BRANCH_NAME="feat/task-unnamed-$(date +%s)" + else + BRANCH_NAME="feat/consensus-${TASK_SLUG}" + if ! git check-ref-format --branch "${BRANCH_NAME}" >/dev/null 2>&1; then + echo "WARNING: derived branch name '${BRANCH_NAME}' is invalid — falling back to task-unnamed" >&2 + BRANCH_NAME="feat/task-unnamed-$(date +%s)" + fi + fi # Create worktree git worktree add "{{worktree_dir}}/$BRANCH_NAME" -b "$BRANCH_NAME" diff --git a/amplifier-bundle/recipes/default-workflow.yaml b/amplifier-bundle/recipes/default-workflow.yaml index caff65cd4..48a7d94e9 100644 --- a/amplifier-bundle/recipes/default-workflow.yaml +++ b/amplifier-bundle/recipes/default-workflow.yaml @@ -284,10 +284,46 @@ steps: # Fetch latest refs git fetch origin main >&2 + # NOTE: This slug pipeline is duplicated in consensus-workflow.yaml step3-setup-worktree + # with a 30-char limit instead of 50. If you update this pipeline (e.g. security patch), + # update consensus-workflow.yaml in the same commit. See Issue #2952 for context. + # + # ACTION REQUIRED: File a dedicated security issue to audit the recipe runner's + # template substitution engine for single-quote escaping (REQ-SEC-001 audit). + # Until confirmed safe, user-provided values should be injected as env vars, + # not as inline template substitutions in single-quoted bash strings. + # # Generate branch name — use printf to safely capture the shlex-quoted value - TASK_DESC=$(printf '%s' {{task_description}}) - TASK_SLUG=$(echo "$TASK_DESC" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | sed 's/[^a-z0-9-]//g' | cut -c1-50) - BRANCH_NAME="{{branch_prefix}}/issue-{{issue_number}}-${TASK_SLUG}" + # Pipeline stages: + # 1. tr '\n\r' ' ' — CRLF normalisation: collapse multi-paragraph input to single line + # 2. tr '[:upper:]' '[:lower:]' — lowercase + # 3. tr -s ' ' '-' — squeeze runs of spaces into single hyphens + # 4. sed whitelist — strip non-alnum-hyphen (security: removes $, `, ;, &, |) + # 5. sed collapse — collapse consecutive hyphens (arises from stripped chars) + # 6. sed edge-trim — strip leading/trailing hyphens + # 7. cut -c1-50 — hard truncation to 50 chars + # 8. sed trailing — strip trailing hyphen that cut can introduce at position 50 + # NOTE (REQ-SEC-001, tracked in issue #2974): {{task_description}} is injected via + # text substitution before shell parsing. Single-quote wrapping below prevents + # word-splitting and glob expansion but does NOT protect against a single-quote + # character in the input (which would terminate the string prematurely). Issue #2974 + # tracks migrating to env-var injection in the recipe runner so values are never + # inlined into shell source. + TASK_DESC=$(printf '%s' '{{task_description}}') + TASK_SLUG=$(printf '%s' "$TASK_DESC" | tr '\n\r' ' ' | tr '[:upper:]' '[:lower:]' | tr -s ' ' '-' | sed 's/[^a-z0-9-]//g' | sed 's/-\{2,\}/-/g' | sed 's/^-//;s/-$//' | cut -c1-50 | sed 's/-$//') + # Guard: empty slug (blank/all-symbol input) produces a trailing-hyphen branch that + # git check-ref-format accepts as valid but is undesirable. Catch it explicitly first. + if [ -z "$TASK_SLUG" ]; then + echo "WARNING: task_description produced an empty slug — falling back to task-unnamed" >&2 + BRANCH_NAME="{{branch_prefix}}/task-unnamed-$(date +%s)" + else + BRANCH_NAME="{{branch_prefix}}/issue-{{issue_number}}-${TASK_SLUG}" + # Validate with authoritative git ref checker; fall back to safe unique name on failure + if ! git check-ref-format --branch "${BRANCH_NAME}" >/dev/null 2>&1; then + echo "WARNING: derived branch name '${BRANCH_NAME}' is invalid — falling back to task-unnamed" >&2 + BRANCH_NAME="{{branch_prefix}}/task-unnamed-$(date +%s)" + fi + fi WORKTREE_PATH="{{repo_path}}/worktrees/${BRANCH_NAME}" echo "Branch: ${BRANCH_NAME}" >&2 diff --git a/docs/features/branch-name-generation.md b/docs/features/branch-name-generation.md new file mode 100644 index 000000000..d5f12a5c0 --- /dev/null +++ b/docs/features/branch-name-generation.md @@ -0,0 +1,407 @@ +# Branch Name Generation in default-workflow + +**Hardened pipeline that converts any `task_description` into a valid, safe git branch name.** + +--- + +## Overview + +Step 4 of `default-workflow` (`step-04-setup-worktree`) automatically generates a git branch name from the `task_description` context variable. The generation pipeline is designed to be robust against: + +- Multi-paragraph task descriptions containing newlines +- Unicode and special characters +- Excessively long descriptions +- Pathological input that could corrupt the git ref namespace or inject shell commands + +The result is always a branch name that: + +- Contains only lowercase alphanumeric characters and hyphens +- Is at most 50 characters long (in the slug portion) +- Passes `git check-ref-format --branch` validation +- Has a safe, predictable fallback when validation fails + +--- + +## Generated Branch Name Format + +``` +{branch_prefix}/issue-{issue_number}-{task_slug} +``` + +**Examples:** + +| `task_description` | `branch_prefix` | `issue_number` | Generated branch | +| ----------------------------------------------------- | --------------- | -------------- | ---------------------------------------------------------------- | +| `Add user authentication` | `feat` | `123` | `feat/issue-123-add-user-authentication` | +| `Fix the login bug\n\nUsers cannot log in on Safari.` | `fix` | `456` | `fix/issue-456-fix-the-login-bug-users-cannot-log-in-on-safari` | +| `Réfactoriser l'API d'authentification` | `refactor` | `789` | `refactor/issue-789-rfactoriser-lapi-dauthentification` | +| ` ` (blank) | `feat` | `42` | `feat/task-unnamed-` (fallback) | + +--- + +## Pipeline Stages + +The branch name is generated by piping `task_description` through eight sequential transforms: + +```bash +TASK_DESC=$(printf '%s' '{{task_description}}') +TASK_SLUG=$(printf '%s' "$TASK_DESC" \ + | tr '\n\r' ' ' \ + | tr '[:upper:]' '[:lower:]' \ + | tr -s ' ' '-' \ + | sed 's/[^a-z0-9-]//g' \ + | sed 's/-\{2,\}/-/g' \ + | sed 's/^-//;s/-$//' \ + | cut -c1-50 \ + | sed 's/-$//') +if [ -z "$TASK_SLUG" ]; then + echo "WARNING: task_description produced an empty slug — falling back to task-unnamed" >&2 + BRANCH_NAME="{{branch_prefix}}/task-unnamed-$(date +%s)" +else + BRANCH_NAME="{{branch_prefix}}/issue-{{issue_number}}-${TASK_SLUG}" + if ! git check-ref-format --branch "${BRANCH_NAME}" >/dev/null 2>&1; then + echo "WARNING: derived branch name '${BRANCH_NAME}' is invalid — falling back to task-unnamed" >&2 + BRANCH_NAME="{{branch_prefix}}/task-unnamed-$(date +%s)" + fi +fi +``` + +### Stage 1: Safe capture — `printf '%s'` + +```bash +TASK_DESC=$(printf '%s' '{{task_description}}') +``` + +The template variable is rendered inside single quotes so the shell never interprets its contents. `printf '%s'` captures the value verbatim, including any embedded newlines, without triggering word-splitting or glob expansion. + +### Stage 2: Newline normalisation — `tr '\n\r' ' '` + +```bash +tr '\n\r' ' ' +``` + +Converts both Unix (`\n`) and Windows (`\r`) line endings to spaces. This is the root-cause fix for [issue #2952](#issue-2952) — multi-paragraph descriptions previously allowed raw newlines to flow into the slug, producing invalid branch names. + +**Before this stage a description like:** + +``` +Fix the login bug + +Users cannot log in on Safari when using SSO. +``` + +would produce a branch name with a literal newline in it. + +**After this stage** it becomes a single space-separated string: + +``` +Fix the login bug Users cannot log in on Safari when using SSO. +``` + +### Stage 3: Lowercase — `tr '[:upper:]' '[:lower:]'` + +```bash +tr '[:upper:]' '[:lower:]' +``` + +Converts all ASCII uppercase letters to lowercase. Branch names are case-insensitive on case-insensitive filesystems (macOS HFS+) and conventionally lowercase on all platforms. + +### Stage 4: Space-to-hyphen squeeze — `tr -s ' ' '-'` + +```bash +tr -s ' ' '-' +``` + +The `-s` (squeeze) flag converts **one or more** consecutive spaces into a single hyphen. This eliminates double-hyphen runs that would otherwise appear wherever consecutive spaces came from `tr '\n\r'` (stage 2) or multiple spaces in the original description. + +### Stage 5: Whitelist sanitisation — `sed` + +```bash +sed 's/[^a-z0-9-]//g; s/-\{2,\}/-/g; s/^-//; s/-$//' +``` + +Four substitutions in one `sed` invocation: + +| Expression | Effect | +| ------------------------ | --------------------------------------------------- | +| `s/[^a-z0-9-]//g` | Strips every character that is not `a-z`, `0-9`, or `-` | +| `s/-\{2,\}/-/g` | Collapses two-or-more consecutive hyphens to one | +| `s/^-//` | Strips a leading hyphen (would make an invalid ref) | +| `s/-$//` | Strips a trailing hyphen (would make an invalid ref) | + +The whitelist expression (`[^a-z0-9-]`) also removes characters that could be interpreted as shell metacharacters (`$`, `` ` ``, `;`, `&`, `|`, `(`, `)`) — this stage is the primary command-injection defence. + +### Stage 6: Length cap — `cut -c1-50` + +```bash +cut -c1-50 +``` + +Truncates the slug to at most 50 characters. Git itself has no hard branch-name length limit, but the full branch name (prefix + issue number + slug) must fit within remote URL limits and terminal display widths. 50 characters for the slug portion is conservative and leaves room for all standard prefixes. + +### Stage 7: Trailing-hyphen strip — `sed 's/-$//'` + +```bash +sed 's/-$//' +``` + +`cut -c1-50` can produce a trailing hyphen when the character at position 50 happens to be a hyphen — for example, a description that generates `add-oauth-support-and-user-authentication-flow-` (50 chars). Stage 5 already strips trailing hyphens, but it runs **before** `cut`, so any hyphen introduced by truncation must be removed again here. + +This stage is a no-op on the vast majority of inputs; it fires only when `cut` lands exactly on a hyphen character. + +### Stage 8: Validation gate — empty-slug guard + `git check-ref-format` + +```bash +# Guard: empty slug produces a trailing-hyphen branch (e.g. feat/issue-42-) +# that git check-ref-format accepts as valid but is undesirable. +if [ -z "$TASK_SLUG" ]; then + echo "WARNING: task_description produced an empty slug — falling back to task-unnamed" >&2 + BRANCH_NAME="{{branch_prefix}}/task-unnamed-$(date +%s)" +else + BRANCH_NAME="{{branch_prefix}}/issue-{{issue_number}}-${TASK_SLUG}" + if ! git check-ref-format --branch "${BRANCH_NAME}" >/dev/null 2>&1; then + echo "WARNING: derived branch name '${BRANCH_NAME}' is invalid — falling back to task-unnamed" >&2 + BRANCH_NAME="{{branch_prefix}}/task-unnamed-$(date +%s)" + fi +fi +``` + +The gate has two layers: + +1. **Empty-slug guard**: If the pipeline produces an empty slug (blank or all-symbol input), `git check-ref-format` would accept the resulting `feat/issue-42-` name (trailing hyphen is git-valid). The explicit `[ -z "$TASK_SLUG" ]` check catches this case before the git command runs. + +2. **`git check-ref-format --branch`**: The authoritative oracle for branch-name validity. Catches edge cases such as a branch prefix that contains invalid characters. + +On failure, the branch name falls back to `{{branch_prefix}}/task-unnamed-`. The timestamp suffix prevents "branch already exists" errors when the fallback is triggered on multiple consecutive runs. The raw `task_description` is **never** logged in the warning message; only the sanitised (and therefore safe) `BRANCH_NAME` is printed. + +--- + +## Configuration + +Branch name generation uses three context variables supplied when invoking the workflow: + +| Variable | Required | Description | Example | +| ------------------ | -------- | -------------------------------------------- | -------------------- | +| `task_description` | Yes | Human-readable description of the task | `"Add OAuth support"` | +| `issue_number` | Yes | Issue or ticket number (numeric or string) | `"2952"` | +| `branch_prefix` | Yes | Git branch prefix (conventionally `feat`, `fix`, `docs`, `refactor`, `test`) | `"feat"` | +| `repo_path` | Yes | Absolute path to the git repository | `"/home/user/myapp"` | + +**Branch prefix conventions:** + +| Prefix | Use for | +| ---------- | -------------------------------- | +| `feat` | New features | +| `fix` | Bug fixes | +| `docs` | Documentation-only changes | +| `refactor` | Code restructuring without behaviour change | +| `test` | Adding or updating tests | +| `chore` | Build, tooling, dependency updates | + +--- + +## Usage + +### Run default-workflow with a simple description + +```bash +amplihack recipe run default-workflow --context '{ + "task_description": "Add user authentication with OAuth", + "issue_number": "123", + "branch_prefix": "feat", + "repo_path": "/home/user/myapp" +}' +# Creates branch: feat/issue-123-add-user-authentication-with-oauth +``` + +### Run with a multi-paragraph description + +```bash +amplihack recipe run default-workflow --context-file task.json +``` + +Where `task.json` contains: + +```json +{ + "task_description": "Fix login timeout\n\nUsers are logged out after 5 minutes instead of 30.\nThis happens on all browsers. The session cookie is set correctly\nbut the server-side token expires too early.", + "issue_number": "456", + "branch_prefix": "fix", + "repo_path": "/home/user/myapp" +} +``` + +Generated branch: `fix/issue-456-fix-login-timeout-users-are-logged-out` + +The pipeline converts newlines to spaces and truncates to 50 characters, so the full multi-paragraph description is safely reduced to a valid branch name. + +### Dry-run to preview the branch name + +```bash +amplihack recipe run default-workflow --dry-run --context '{ + "task_description": "Refactor the authentication module", + "issue_number": "789", + "branch_prefix": "refactor", + "repo_path": "/home/user/myapp" +}' +# Shows the generated branch name without creating the worktree +``` + +--- + +## Fallback Behaviour + +When `git check-ref-format --branch` rejects the generated name, the workflow uses `{{branch_prefix}}/task-unnamed-` as the fallback branch name. + +**Conditions that trigger the fallback:** + +| Condition | Example input | Fallback branch | +| --------- | ------------- | --------------- | +| `task_description` is empty or whitespace-only | `" "` | `feat/task-unnamed-1741478400` (empty-slug guard fires before git check) | +| Slug becomes empty after sanitisation (all-symbol input) | `"!!!???###"` | `feat/task-unnamed-1741478400` (empty-slug guard fires before git check) | +| `branch_prefix` itself is invalid | `"feat name"` | `feat name/task-unnamed-...` (still invalid; `git worktree add` will fail — fix the prefix) | +| `issue_number` is empty, producing `feat/issue--slug` | `""` | `feat/issue--slug` (git accepts double-hyphen; branch is created with this name) | + +The Unix timestamp suffix ensures each fallback branch is unique, preventing "branch already exists" errors when the fallback fires on multiple consecutive runs (e.g., CI retrying a workflow with the same pathological input). + +--- + +## Security Considerations + +The branch name pipeline provides several layers of protection against malicious or accidental input: + +| Threat | Mitigation | +| ------ | ---------- | +| Shell command injection via `task_description` | `printf '%s' '...'` renders in single quotes (prevents word-splitting and `$()` injection); `sed 's/[^a-z0-9-]//g'` removes all shell metacharacters from the slug | +| Single-quote injection in `task_description` | **Known limitation (REQ-SEC-001, issue #2974):** a `'` in the input breaks the single-quote wrapping. Full mitigation requires env-var injection in the recipe runner (tracked in #2974) | +| CRLF injection | `tr '\n\r' ' '` converts all line endings to spaces before any other processing | +| Path traversal via `..` in branch name | `sed 's/[^a-z0-9-]//g'` removes `.` | +| Branch name exceeding remote URL limits | `cut -c1-50` enforces a hard upper bound | +| Trailing hyphen from `cut` truncation | `sed 's/-$//'` after `cut` strips any hyphen left at position 50 | +| Invalid ref reaching `git worktree add` | `git check-ref-format --branch` validates before the git command runs | +| Fallback branch collision (repeated failures) | Fallback name includes `$(date +%s)` Unix timestamp for uniqueness | +| Secrets or PII leaking into git history | Warning message logs only the sanitised `BRANCH_NAME`, never the raw `task_description` | + +> **Important:** Branch names derived from `task_description` appear in git history, remote branch listings, pull request titles, and CI logs. Do not include secrets, passwords, API keys, or PII in `task_description`. + +--- + +## Troubleshooting + +### Branch name always falls back to `task-unnamed` + +**Symptom:** Every run creates a `feat/task-unnamed` branch regardless of input. + +**Diagnosis:** + +1. Check that `issue_number` is set and non-empty: + + ```bash + amplihack recipe show default-workflow --steps-only | grep issue_number + ``` + +2. Check that `branch_prefix` is a valid git component (no spaces, slashes, or special characters): + + ```bash + git check-ref-format --branch "feat/issue-1-test" # should exit 0 + git check-ref-format --branch "my prefix/issue-1-test" # will exit 1 + ``` + +3. Check if `task_description` is entirely non-alphanumeric: + + ```bash + echo "!!!???###" | tr '\n\r' ' ' | tr '[:upper:]' '[:lower:]' | tr -s ' ' '-' | sed 's/[^a-z0-9-]//g' + # Output: (empty) — will trigger fallback + ``` + +### Branch name is unexpectedly truncated + +The slug portion is capped at 50 characters. Descriptions longer than ~50 characters will be truncated. This is intentional. The first 50 characters should be descriptive enough to identify the branch. Use the issue number and PR title for full context. + +### Two runs with the same description create conflicting branches + +If `issue_number` is the same across two runs, the branch name will be identical. The second `git worktree add` will fail with "branch already exists". Assign a unique `issue_number` to each task or delete the existing branch first: + +```bash +git branch -d feat/issue-123-add-user-authentication +``` + +### Worktree path contains unexpected hyphens + +Multi-word descriptions with consecutive spaces or punctuation produce multiple hyphens that are then collapsed. `"Add OAuth support"` (double spaces) becomes `add-oauth-support`, not `add--oauth--support`. This is the intended behaviour of `tr -s ' ' '-'`. + +--- + +## Technical Reference + +### Full pipeline (annotated) + +```bash +# Stage 1: safely capture template variable — single-quote prevents word-splitting and +# glob expansion. NOTE (REQ-SEC-001, issue #2974): a single-quote in the input value +# would break out of this string. Full fix requires env-var injection in the runner. +TASK_DESC=$(printf '%s' '{{task_description}}') + +TASK_SLUG=$(printf '%s' "$TASK_DESC" \ + | tr '\n\r' ' ' \ # Stage 2: normalise newlines → spaces + | tr '[:upper:]' '[:lower:]' \ # Stage 3: lowercase + | tr -s ' ' '-' \ # Stage 4: spaces → single hyphen + # Stage 5: whitelist + normalise hyphens + # 5a: strip every char that is not a-z, 0-9, or hyphen (also removes shell metacharacters) + # 5b: collapse two-or-more consecutive hyphens to one + # 5c: strip a leading hyphen (would make an invalid ref) + # 5d: strip a trailing hyphen (would make an invalid ref) + | sed 's/[^a-z0-9-]//g' \ + | sed 's/-\{2,\}/-/g' \ + | sed 's/^-//;s/-$//' \ + | cut -c1-50 \ # Stage 6: truncate slug to 50 chars + | sed 's/-$//') # Stage 7: strip trailing hyphen cut can leave + +# Stage 8a: empty-slug guard — git check-ref-format accepts trailing-hyphen branches +# (e.g. feat/issue-42-) as valid, so we must catch an empty slug explicitly. +if [ -z "$TASK_SLUG" ]; then + echo "WARNING: task_description produced an empty slug — falling back to task-unnamed" >&2 + BRANCH_NAME="{{branch_prefix}}/task-unnamed-$(date +%s)" +else + # Assemble full branch name + BRANCH_NAME="{{branch_prefix}}/issue-{{issue_number}}-${TASK_SLUG}" + # Stage 8b: authoritative validation gate — timestamp suffix prevents fallback collisions + if ! git check-ref-format --branch "${BRANCH_NAME}" >/dev/null 2>&1; then + echo "WARNING: derived branch name '${BRANCH_NAME}' is invalid — falling back to task-unnamed" >&2 + BRANCH_NAME="{{branch_prefix}}/task-unnamed-$(date +%s)" + fi +fi +``` + +### Portability notes + +- `tr '\n\r' ' '` — POSIX `tr`; works on GNU coreutils, BSD `tr`, and BusyBox. +- `sed 's/-\{2,\}/-/g'` — POSIX BRE; `\{2,\}` is supported everywhere. Equivalent ERE: `sed -E 's/-{2,}/-/g'`. +- `cut -c1-50` — POSIX; counts bytes, not Unicode codepoints. For multi-byte UTF-8 input the cut position may fall inside a multi-byte sequence, producing a trailing invalid byte. The subsequent `git check-ref-format` call will reject such a name and use the fallback. + +### Acceptance criteria (issue #2952) + +| Criterion | How it is met | +| --------- | -------------- | +| No newlines in branch name | `tr '\n\r' ' '` (Stage 2) converts before any other transform | +| Max 50 characters in slug | `cut -c1-50` (Stage 6) | +| No trailing hyphen in slug | `sed 's/-$//'` (Stage 7) runs after `cut`; empty-slug guard (Stage 8) prevents the trailing-hyphen branch that `git check-ref-format` would otherwise accept | +| `git check-ref-format` exits 0 | Empty-slug guard + `git check-ref-format` validation (Stage 8) | +| Safe fallback on invalid name | Falls back to `{{branch_prefix}}/task-unnamed-` with stderr warning | +| Fallback uniqueness | Timestamp suffix (`date +%s`) prevents collision when fallback fires on repeated runs | +| Single-line short descriptions produce identical output | New stages 2–8 are no-ops on already-valid input | + +--- + +## Related Documentation + +- [Default Workflow](../claude/workflow/DEFAULT_WORKFLOW.md) — Full 22-step development process +- [Main Branch Protection](main-branch-protection.md) — Preventing direct commits to `main` +- [Recipe CLI Commands](../howto/recipe-cli-commands.md) — Running and configuring recipes +- [Worktree Support](../worktree-support.md) — Power Steering in git worktrees +- [Worktree Troubleshooting](../power-steering-worktree-troubleshooting.md) — Resolving worktree issues + +--- + +**Last Updated:** 2026-03-09 +**Minimum Requirements:** Git 2.5+ (worktree support), POSIX shell (bash/sh), GNU or BSD coreutils diff --git a/pyproject.toml b/pyproject.toml index 8d989dcfc..48ed3da6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ backend-path = ["."] [project] name = "amplihack" -version = "0.6.3" +version = "0.6.4" description = "Amplifier bundle for agentic coding with comprehensive skills, recipes, and workflows" requires-python = ">=3.11" dependencies = [