Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,6 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/td.rb
git diff --cached --quiet && echo "No changes" && exit 0
# Allowed by scripts/commit-msg.sh as an automated release exception.
git commit -m "td: bump to ${{ steps.version.outputs.version }}"
git push
13 changes: 5 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,8 @@ go test ./... # Test all

```bash
# Commit changes with proper message
git add .
git commit -m "feat: description of changes

Details here

🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"
git add <specific files>
git commit -m "feat(scope): concise summary (td-a1b2)"

# Create version tag (bump from current version, e.g., v0.2.0 → v0.3.0)
git tag -a v0.3.0 -m "Release v0.3.0: description"
Expand All @@ -56,6 +50,9 @@ go install -ldflags "-X main.Version=v0.3.0" ./...
td version
```

Human-authored task commits should use `type[(scope)]: summary (td-<id>)`.
The automated Homebrew tap release commit is the approved exception: `td: bump to vX.Y.Z`.

## Architecture

- `cmd/` - Cobra commands
Expand Down
13 changes: 9 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help fmt test install tag release check-clean check-version install-hooks
.PHONY: help fmt test test-hooks install tag release check-clean check-version install-hooks

SHELL := /bin/sh

Expand All @@ -13,7 +13,8 @@ help:
@printf "%s\n" \
"Targets:" \
" make fmt # gofmt -w ." \
" make install-hooks # install git pre-commit hook" \
" make test-hooks # test git hook scripts" \
" make install-hooks # install git pre-commit and commit-msg hooks" \
" make test # go test ./..." \
" make install # build and install with version from git" \
" make tag VERSION=vX.Y.Z # create annotated git tag (requires clean tree)" \
Expand All @@ -25,6 +26,9 @@ fmt:
test:
go test ./...

test-hooks:
bash ./scripts/test_commit-msg.sh

install:
@V="$(GIT_DESCRIBE)"; V=$${V:-dev}; \
echo "Installing td $$V"; \
Expand Down Expand Up @@ -52,6 +56,7 @@ release: tag
git push origin "$(VERSION)"

install-hooks:
@echo "Installing git pre-commit hook..."
@echo "Installing git hooks..."
@ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit
@echo "Done. Hook installed at .git/hooks/pre-commit"
@ln -sf ../../scripts/commit-msg.sh .git/hooks/commit-msg
@echo "Done. Hooks installed at .git/hooks/pre-commit and .git/hooks/commit-msg"
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,16 +189,22 @@ make install-dev
# Format code
make fmt

# Install git pre-commit hook (gofmt, go vet, go build on staged files)
# Install git hooks (pre-commit checks + commit message normalization)
make install-hooks
```

Commit subjects for task work use the canonical format `type[(scope)]: summary (td-<id>)`.
Example: `feat(sync): persist per-device cursor state (td-a1b2)`.

## Tests & Quality Checks

```bash
# Run all tests (114 tests across cmd/, internal/db/, internal/models/, etc.)
make test

# Test git hook scripts
make test-hooks

# Expected output: ok for each package, ~2s total runtime
# Example:
# ok github.com/marcus/td/cmd 1.994s
Expand Down
20 changes: 18 additions & 2 deletions docs/guides/releasing-new-version.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Add entry at the top of `CHANGELOG.md`:
Commit the changelog:
```bash
git add CHANGELOG.md
git commit -m "docs: Update changelog for vX.Y.Z"
git commit -m "docs: update changelog for vX.Y.Z (td-<id>)"
```

### 3. Verify Tests Pass
Expand Down Expand Up @@ -137,7 +137,7 @@ go test ./...
# Update changelog
# (Edit CHANGELOG.md, add entry at top)
git add CHANGELOG.md
git commit -m "docs: Update changelog for vX.Y.Z"
git commit -m "docs: update changelog for vX.Y.Z (td-<id>)"

# Push commits, then tag (tag push triggers automated release)
git push origin main
Expand All @@ -164,3 +164,19 @@ brew upgrade td && td version
- [ ] Homebrew tap updated in `marcus/homebrew-tap` (automated)
- [ ] `brew install marcus/tap/td` works
- [ ] `td version` shows correct version

## Commit Message Format

Human-authored task commits use `type[(scope)]: summary (td-<id>)`.
Examples:

```bash
git commit -m "docs: update changelog for vX.Y.Z (td-a1b2)"
git commit -m "fix(release): tighten release guide wording (td-c3d4)"
```

The automated Homebrew tap update created by `.github/workflows/release.yml` is the approved exception:

```text
td: bump to vX.Y.Z
```
101 changes: 101 additions & 0 deletions scripts/commit-msg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# commit-msg hook for td
# Install: make install-hooks (or: ln -sf ../../scripts/commit-msg.sh .git/hooks/commit-msg)
set -euo pipefail

if [[ $# -ne 1 ]]; then
echo "usage: $0 <commit-message-file>" >&2
exit 1
fi

msg_file=$1
subject_pattern='^([[:alpha:]][[:alnum:]-]*)(\([^()]+\))?:[[:space:]]*(.+)$'
paren_task_pattern='^(.*[^[:space:]])[[:space:]]+\((td-[[:alnum:]]+)\)$'
bare_task_pattern='^(.*[^[:space:]])[[:space:]]+(td-[[:alnum:]]+)$'

trim() {
local value=$1
value=${value#"${value%%[![:space:]]*}"}
value=${value%"${value##*[![:space:]]}"}
printf '%s' "$value"
}

subject=""
if IFS= read -r subject <"$msg_file"; then
:
fi
subject=$(trim "$subject")

if [[ -z "$subject" ]]; then
echo "commit message subject is required" >&2
exit 1
fi

# Preserve Git's generated subjects and the automated Homebrew tap bump commit.
if [[ "$subject" =~ ^(Merge|Revert|fixup\!|squash\!)\ ]]; then
exit 0
fi
if [[ "$subject" =~ ^td:\ bump\ to\ v[0-9]+\.[0-9]+\.[0-9]+([.-][[:alnum:]]+)*$ ]]; then
exit 0
fi

if [[ ! "$subject" =~ $subject_pattern ]]; then
cat >&2 <<'EOF'
invalid commit message subject

Expected format:
type: summary (td-<id>)
type(scope): summary (td-<id>)

Example:
feat(sync): persist cursor handling (td-a1b2)
EOF
exit 1
fi

prefix=${BASH_REMATCH[1]}
scope=${BASH_REMATCH[2]:-}
rest=$(trim "${BASH_REMATCH[3]}")

summary=
task_ref=
if [[ "$rest" =~ $paren_task_pattern ]]; then
summary=$(trim "${BASH_REMATCH[1]}")
task_ref=${BASH_REMATCH[2]}
elif [[ "$rest" =~ $bare_task_pattern ]]; then
summary=$(trim "${BASH_REMATCH[1]}")
task_ref=${BASH_REMATCH[2]}
else
cat >&2 <<'EOF'
missing trailing task reference in commit message subject

Expected format:
type: summary (td-<id>)
type(scope): summary (td-<id>)

Example:
fix(db): preserve audit trail ordering (td-a1b2)
EOF
exit 1
fi

if [[ -z "$summary" ]]; then
echo "commit message summary cannot be empty" >&2
exit 1
fi

normalized="${prefix}${scope}: ${summary} (${task_ref})"

if [[ "$normalized" == "$subject" ]]; then
exit 0
fi

tmp_file=$(mktemp)
trap 'rm -f "$tmp_file"' EXIT

printf '%s\n' "$normalized" >"$tmp_file"
if [[ $(wc -l <"$msg_file") -gt 1 ]]; then
tail -n +2 "$msg_file" >>"$tmp_file"
fi

mv "$tmp_file" "$msg_file"
4 changes: 2 additions & 2 deletions scripts/loop-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Batch review loops:

```bash
git add <specific files>
git commit -m "feat: <summary> (td-<id>)"
git commit -m "feat(scope): <summary> (td-<id>)"
td review <id>
```

Expand All @@ -174,4 +174,4 @@ Use `td review`, not `td close` — self-closing is blocked.
- **Don't break sync.** Deterministic IDs, proper event logging, no hard deletes.
- **Session isolation is sacred.** Don't bypass review guards.
- **If stuck, log and skip.** `td log <id> "Blocked: <reason>"` then `td block <id>`.
- **Commit messages reference td.** Format: `feat|fix|chore: <summary> (td-<id>)`
- **Commit messages reference td.** Format: `type[(scope)]: <summary> (td-<id>)`
115 changes: 115 additions & 0 deletions scripts/test_commit-msg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
HOOK="$ROOT/scripts/commit-msg.sh"

pass_count=0

make_msg_file() {
local path=$1
printf '%s' "$2" >"$path"
}

run_success_case() {
local name=$1
local input=$2
local expected=$3
local msg_file
msg_file=$(mktemp)
make_msg_file "$msg_file" "$input"

"$HOOK" "$msg_file"

local actual
actual=$(cat "$msg_file")
if [[ "$actual" != "$expected" ]]; then
echo "FAIL: $name"
echo "expected:"
printf '%s\n' "$expected"
echo "actual:"
printf '%s\n' "$actual"
rm -f "$msg_file"
exit 1
fi

rm -f "$msg_file"
pass_count=$((pass_count + 1))
}

run_failure_case() {
local name=$1
local input=$2
local expected_stderr=$3
local msg_file
local err_file
msg_file=$(mktemp)
err_file=$(mktemp)
make_msg_file "$msg_file" "$input"

if "$HOOK" "$msg_file" 2>"$err_file"; then
echo "FAIL: $name"
echo "expected hook to fail"
rm -f "$msg_file" "$err_file"
exit 1
fi

if ! grep -Fq "$expected_stderr" "$err_file"; then
echo "FAIL: $name"
echo "expected stderr to contain: $expected_stderr"
echo "actual stderr:"
cat "$err_file"
rm -f "$msg_file" "$err_file"
exit 1
fi

rm -f "$msg_file" "$err_file"
pass_count=$((pass_count + 1))
}

run_success_case \
"normalizes plain subject" \
$'feat: normalize hook output td-a1b2\n\nBody stays put.\n' \
$'feat: normalize hook output (td-a1b2)\n\nBody stays put.'

run_success_case \
"accepts canonical subject" \
$'fix: preserve body content (td-a1b2)\n\nNightshift-Task: commit-normalize\n' \
$'fix: preserve body content (td-a1b2)\n\nNightshift-Task: commit-normalize'

run_success_case \
"normalizes scoped subject" \
$'feat(sync): persist cursor td-a1b2' \
$'feat(sync): persist cursor (td-a1b2)'

run_success_case \
"uses trailing task token when summary mentions another td id" \
$'feat: mention td-a1b2 parsing before final ref (td-c3d4)' \
$'feat: mention td-a1b2 parsing before final ref (td-c3d4)'

run_success_case \
"normalizes trailing task token when summary mentions another td id" \
$'feat(sync): mention td-a1b2 parsing before final ref td-c3d4' \
$'feat(sync): mention td-a1b2 parsing before final ref (td-c3d4)'

run_success_case \
"allows automated release bump commits" \
'td: bump to v1.2.3' \
'td: bump to v1.2.3'

run_success_case \
"allows merge subjects" \
'Merge branch '\''feature/example'\''' \
'Merge branch '\''feature/example'\'''

run_failure_case \
"rejects missing task reference" \
'feat: missing task reference' \
'missing trailing task reference'

run_failure_case \
"rejects malformed prefix" \
'feat sync: malformed prefix td-a1b2' \
'invalid commit message subject'

echo "commit-msg hook tests passed ($pass_count cases)"