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
139 changes: 138 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ on:
release:
types:
- published
workflow_dispatch:
inputs:
tag:
description: "Release tag to regenerate the Homebrew formula for (e.g. v1.3.1)"
required: true
type: string

permissions:
contents: write
Expand All @@ -12,6 +18,7 @@ permissions:
jobs:
releases-matrix:
name: Release Go Binary
if: github.event_name == 'release'
runs-on: ubuntu-latest
strategy:
matrix:
Expand All @@ -32,4 +39,134 @@ jobs:
goarch: ${{ matrix.goarch }}
binary_name: "rune"
ldflags: "-w -s -X 'github.com/ArjenSchwarz/rune/cmd.Version=${{ env.APP_VERSION }}' -X 'github.com/ArjenSchwarz/rune/cmd.BuildTime=${{ env.BUILD_TIME }}' -X 'github.com/ArjenSchwarz/rune/cmd.GitCommit=${{ env.GIT_COMMIT }}'"
extra_files: "LICENSE README.md"
extra_files: "LICENSE README.md"
sha256sum: true

homebrew:
name: Update Homebrew tap
needs: releases-matrix
if: always() && needs.releases-matrix.result != 'failure' && needs.releases-matrix.result != 'cancelled'
runs-on: macos-latest
concurrency:
group: homebrew-${{ github.repository }}
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
- name: Resolve release tag
id: tag
env:
INPUT_TAG: ${{ github.event.inputs.tag }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="$INPUT_TAG"
else
TAG="${GITHUB_REF_NAME}"
fi
if [ -z "$TAG" ]; then
echo "No tag resolved" >&2
exit 1
fi
VERSION="${TAG#v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Download sha256 sidecars
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.tag.outputs.tag }}
run: |
gh release download "$TAG" -R "$GITHUB_REPOSITORY" \
-p 'rune-*-darwin-*.tar.gz.sha256' \
-p 'rune-*-linux-*.tar.gz.sha256'
ls -1 rune-*.tar.gz.sha256
- name: Parse sha256 digests
id: digests
env:
VERSION: ${{ steps.tag.outputs.version }}
run: |
DARWIN_ARM64=$(awk '{print $1}' "rune-v${VERSION}-darwin-arm64.tar.gz.sha256")
DARWIN_AMD64=$(awk '{print $1}' "rune-v${VERSION}-darwin-amd64.tar.gz.sha256")
LINUX_ARM64=$(awk '{print $1}' "rune-v${VERSION}-linux-arm64.tar.gz.sha256")
LINUX_AMD64=$(awk '{print $1}' "rune-v${VERSION}-linux-amd64.tar.gz.sha256")
for v in "$DARWIN_ARM64" "$DARWIN_AMD64" "$LINUX_ARM64" "$LINUX_AMD64"; do
if [ -z "$v" ] || [ ${#v} -ne 64 ]; then
echo "Invalid sha256 digest: $v" >&2
exit 1
fi
done
echo "darwin_arm64=$DARWIN_ARM64" >> "$GITHUB_OUTPUT"
echo "darwin_amd64=$DARWIN_AMD64" >> "$GITHUB_OUTPUT"
echo "linux_arm64=$LINUX_ARM64" >> "$GITHUB_OUTPUT"
echo "linux_amd64=$LINUX_AMD64" >> "$GITHUB_OUTPUT"
- name: Render Formula/rune.rb
env:
VERSION: ${{ steps.tag.outputs.version }}
DARWIN_ARM64: ${{ steps.digests.outputs.darwin_arm64 }}
DARWIN_AMD64: ${{ steps.digests.outputs.darwin_amd64 }}
LINUX_ARM64: ${{ steps.digests.outputs.linux_arm64 }}
LINUX_AMD64: ${{ steps.digests.outputs.linux_amd64 }}
run: |
mkdir -p Formula
cat > Formula/rune.rb <<EOF
class Rune < Formula
desc "CLI for managing hierarchical markdown task lists"
homepage "https://github.com/ArjenSchwarz/rune"
version "${VERSION}"

on_macos do
if Hardware::CPU.arm?
url "https://github.com/ArjenSchwarz/rune/releases/download/v#{version}/rune-v#{version}-darwin-arm64.tar.gz"
sha256 "${DARWIN_ARM64}"
else
url "https://github.com/ArjenSchwarz/rune/releases/download/v#{version}/rune-v#{version}-darwin-amd64.tar.gz"
sha256 "${DARWIN_AMD64}"
end
end

on_linux do
if Hardware::CPU.arm?
url "https://github.com/ArjenSchwarz/rune/releases/download/v#{version}/rune-v#{version}-linux-arm64.tar.gz"
sha256 "${LINUX_ARM64}"
else
url "https://github.com/ArjenSchwarz/rune/releases/download/v#{version}/rune-v#{version}-linux-amd64.tar.gz"
sha256 "${LINUX_AMD64}"
end
end

def install
bin.install "rune"
end

test do
system "#{bin}/rune", "--version"
end
end
EOF
cat Formula/rune.rb
- name: brew audit
run: brew audit --strict --online ./Formula/rune.rb
- name: brew install + test
run: |
brew install --formula ./Formula/rune.rb
brew test rune
- name: Checkout tap repo
uses: actions/checkout@v4
with:
repository: ArjenSchwarz/homebrew-rune
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: homebrew-rune
- name: Commit formula to tap
env:
TAG: ${{ steps.tag.outputs.tag }}
run: |
mkdir -p homebrew-rune/Formula
cp Formula/rune.rb homebrew-rune/Formula/rune.rb
cd homebrew-rune
git config user.name "rune-release-bot"
git config user.email "rune-release-bot@users.noreply.github.com"
git add Formula/rune.rb
if git diff --cached --quiet; then
echo "Formula already up-to-date for $TAG; nothing to commit."
else
git commit -m "rune $TAG"
git push origin HEAD:main
fi
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Homebrew Install** (T-824): Install rune via `brew install arjenschwarz/rune/rune`. Release workflow now emits `.sha256` sidecars for every platform tarball and runs a new macOS `homebrew` job that renders `Formula/rune.rb` from the sidecars, validates with `brew audit --strict --online` and `brew install`/`brew test`, and commits to the `ArjenSchwarz/homebrew-rune` tap using `HOMEBREW_TAP_TOKEN`. A `workflow_dispatch` trigger with a `tag` input allows re-running the formula update against an existing release without rebuilding binaries. Commits are idempotent (skip when unchanged) and serialised via a concurrency group.

### Changed

- **Configuration**: `.rune.yml` now rejects unknown fields (`KnownFields` enforcement). Config files with extra or misspelled keys that were previously silently ignored will now produce an error. Remove any unsupported fields from your `.rune.yml` to resolve.
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ A standalone Go command-line tool designed for AI agents and developers to creat

## Installation

### Homebrew (macOS/Linux)

```bash
brew install arjenschwarz/rune/rune
```

### Go install

```bash
go install github.com/arjenschwarz/rune@latest
```
Expand Down
9 changes: 9 additions & 0 deletions docs/agent-notes/batch-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ The block-based approach matters because users may intentionally interleave remo
3. When adding new validatable fields to `Operation`, update `validateOperation` to include content validation for both add and update cases

The `validateDetailsAndReferences` helper in batch.go centralises detail/reference content validation for use in `validateOperation`.

## Phase Marker Adjustment

Phase-aware batch adds use `addTaskWithPhaseMarkers` in `internal/task/batch.go`. When inserting a top-level task into an earlier phase, the immediate next phase marker must move to the new task and every later marker must be shifted to account for renumbered top-level tasks. T-787 tracks a bug where the batch path only updates the immediate next marker, which can render later phase headers before the wrong task in files with three or more phases.

## Testing Gotcha: Cobra Flag State

Cobra flag values and `Changed` bits persist across `Execute()` calls in the same process. This matters in tests where multiple tests share `rootCmd`. The `resetBatchFlags()` helper in `batch_test.go` resets `batchInput` and the flag's `Changed` bit. Call it at the start of any batch test that does NOT use `--input` to avoid false positives from stale state.

## Known Gap: Phase Detection for Plain Operations

`cmd/batch.go` currently routes to `ExecuteBatchWithPhases` only when an operation has a `phase` field or type `add-phase`. If the target file already has phase markers but the batch contains only plain operations such as `remove`, it uses `ExecuteBatch` and then `WriteFile`, which reuses original phase markers without adjusting them for removed top-level tasks. T-820 tracks this; the command should detect existing phase markers before choosing the execution path.
1 change: 1 addition & 0 deletions docs/agent-notes/config-discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Uses `exec.CommandContext` with a timeout context to prevent hangs. Key details:
- The timeout test (`TestGetCurrentBranchTimeout`) uses a 200ms timeout with a mock git that sleeps 10s, verifying the function returns within the computed bound
- `TestDiscoverFileFromBranch` tests mock both `getCurrentBranch` and `getRepoRoot` (returning the temp dir) since the temp dirs are not real git repos
- `TestDiscoverFileFromBranchSubdirectory` initializes a real git repo and tests from a subdirectory without mocking `getRepoRoot`
- Home config tests should isolate `HOME` with a temp directory. T-812 tracks `TestConfigPrecedence` writing to the developer's real `~/.config/rune/config.yml`, which breaks restricted sandboxes and can mutate real local config.

## Gotchas

Expand Down
2 changes: 2 additions & 0 deletions docs/agent-notes/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ The dependents map registers tasks even if they lack a StableID (using hierarchi
- Returns warnings listing how many tasks had references cleaned up.
- After cleanup, delegates to `removeTaskRecursive` + `RenumberTasks`.

Known gap: the user-facing `remove` command goes through `RemoveTaskWithPhases`, and batch remove operations call `RemoveTask` directly. Those paths currently bypass `RemoveTaskWithDependents`, so removing a blocker can leave stale `BlockedBy` references behind. The `remove` command also keeps a `*Task` pointer for output before mutating the slice; deleting an earlier task can make the success message report the shifted task's title instead of the removed task's title. The title-output issue is tracked as T-801.

## StableID Assignment

Tasks get StableIDs in two ways:
Expand Down
7 changes: 7 additions & 0 deletions docs/agent-notes/phase-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Phase-Aware Add

`cmd/add.go` uses a separate path when `--phase` is provided: it calls `task.AddTaskToPhase(filename, addParent, addTitle, addPhase)` instead of building `AddOptions` and calling the normal extended add path.

`AddTaskToPhase` in `internal/task/operations.go` currently accepts only file path, parent ID, title, and phase name. It preserves phase markers and inserts top-level tasks at the end of the target phase, but it does not know about stream, owner, blocked-by, requirements, or requirements-file.

T-836 tracks the resulting bug: `rune add --phase ... --stream/--owner/--blocked-by/--requirements` silently creates the phased task while dropping those extended fields.
10 changes: 10 additions & 0 deletions docs/agent-notes/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Testing

## Shared Command State

Command tests often call `runX` helpers directly and share package-level globals such as `format` and `dryRun`. Tests that expect default table output must set and restore `format` explicitly, otherwise earlier JSON/markdown tests can leak state. T-857 tracks the current `TestRunCompleteDryRun` failure.

## Current Known Test Failures

- T-856: `internal/task/phase_test.go` has a stale two-argument call to `RenderMarkdownWithPhases`; the production function now requires a `phaseSource *TaskList`.
- T-859: `cmd.TestRenumberPreservesAllPhaseMarkers` shows `runRenumber` misplacing phase markers for files with gapped/non-sequential top-level IDs. `ExtractPhaseMarkers` already returns sequential IDs, but `cmd/renumber.go` still maps markers through raw file task IDs.
Loading
Loading