From 75da59ce04cfca8c045631a3d7046e5679227632 Mon Sep 17 00:00:00 2001 From: goose-guest Date: Thu, 9 Apr 2026 14:52:49 -0700 Subject: [PATCH 1/2] test: add dispatch notes workflow for testing Signed-off-by: goose-guest --- .github/workflows/dispatch-notes-slack.yml | 555 +++++++++++++++++++++ 1 file changed, 555 insertions(+) create mode 100644 .github/workflows/dispatch-notes-slack.yml diff --git a/.github/workflows/dispatch-notes-slack.yml b/.github/workflows/dispatch-notes-slack.yml new file mode 100644 index 0000000000..11ee864cb7 --- /dev/null +++ b/.github/workflows/dispatch-notes-slack.yml @@ -0,0 +1,555 @@ +# Multi-Repo Dispatch Notes Generator for Slack +# +# Generates dispatch notes using goose AI agent for multiple repositories. +# Compares against the last published dispatch to avoid duplicates. +# +# Repositories monitored: +# - entireio/cli +# - entirehq/entiredb +# - go-git/go-git + +name: Dispatch Notes to Slack + +on: + workflow_dispatch: + inputs: + repos: + description: 'Repositories to include' + required: true + type: choice + options: + - all + - entireio/cli + - entirehq/entiredb + - go-git/go-git + + schedule: + # Every Thursday at 9am UTC (start of dispatch collection) + - cron: '0 9 * * 4' + +permissions: + contents: read + +jobs: + # Get last dispatch info and build repo matrix + prepare: + name: Prepare + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.build.outputs.matrix }} + has_repos: ${{ steps.build.outputs.has_repos }} + last_dispatch_date: ${{ steps.dispatch.outputs.date }} + last_dispatch_file: ${{ steps.dispatch.outputs.file }} + + steps: + - name: Clone entire.io for dispatch info + env: + GH_TOKEN: ${{ secrets.ENTIRE_IO_PAT }} + run: | + git clone --depth 1 "https://x-access-token:${GH_TOKEN}@github.com/entirehq/entire.io.git" /tmp/entire-io + + - name: Get last dispatch info + id: dispatch + run: | + DISPATCH_DIR="/tmp/entire-io/website/src/routes/_content/blog/-content" + + # Get latest dispatch file + LATEST_DISPATCH=$(ls -1 "$DISPATCH_DIR"/*dispatch*.md 2>/dev/null | sort | tail -1) + + if [ -z "$LATEST_DISPATCH" ]; then + echo "::error::No dispatch files found" + exit 1 + fi + + FILENAME=$(basename "$LATEST_DISPATCH") + # Extract date from filename (e.g., 2026-04-06-entire-dispatch-0x0008.md -> 2026-04-06) + DISPATCH_DATE=$(echo "$FILENAME" | grep -oE '^[0-9]{4}-[0-9]{2}-[0-9]{2}') + + echo "file=$LATEST_DISPATCH" >> $GITHUB_OUTPUT + echo "date=$DISPATCH_DATE" >> $GITHUB_OUTPUT + echo "::notice::Last dispatch: $FILENAME (date: $DISPATCH_DATE)" + + # Save dispatch content for later use + cp "$LATEST_DISPATCH" /tmp/last_dispatch.md + + - name: Upload dispatch content + uses: actions/upload-artifact@v4 + with: + name: last-dispatch + path: /tmp/last_dispatch.md + retention-days: 1 + + - name: Build repo matrix + id: build + env: + INPUT_REPOS: ${{ inputs.repos }} + EVENT_NAME: ${{ github.event_name }} + run: | + ALL_REPOS="entireio/cli entirehq/entiredb go-git/go-git" + REPOS=() + + if [ "$EVENT_NAME" = "schedule" ] || [ "$INPUT_REPOS" = "all" ]; then + for repo in $ALL_REPOS; do + REPOS+=("{\"repo\":\"${repo}\"}") + done + else + REPOS+=("{\"repo\":\"${INPUT_REPOS}\"}") + fi + + MATRIX=$(printf '%s\n' "${REPOS[@]}" | jq -sc '{include: .}') + echo "has_repos=true" >> $GITHUB_OUTPUT + echo "matrix=$MATRIX" >> $GITHUB_OUTPUT + + # Post the parent message to start the thread + post-parent: + name: Post Parent Message + runs-on: ubuntu-latest + needs: prepare + outputs: + thread_ts: ${{ steps.post.outputs.thread_ts }} + + steps: + - name: Post parent message + id: post + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }} + LAST_DISPATCH_DATE: ${{ needs.prepare.outputs.last_dispatch_date }} + run: | + TODAY=$(date -u +"%B %d, %Y") + + TEXT=$(printf '🚧 THIS IS A TEST 🚧\n\nπŸ“‹ *Dispatch Notes* β€” %s\n_Changes since last dispatch (%s)_' "$TODAY" "$LAST_DISPATCH_DATE") + + PAYLOAD=$(jq -n \ + --arg channel "$SLACK_CHANNEL_ID" \ + --arg text "$TEXT" \ + '{channel: $channel, text: $text, unfurl_links: false}') + + RESPONSE=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") + + if [ "$(echo "$RESPONSE" | jq -r '.ok')" != "true" ]; then + echo "::error::Slack API error: $(echo "$RESPONSE" | jq -r '.error')" + exit 1 + fi + + THREAD_TS=$(echo "$RESPONSE" | jq -r '.ts') + echo "thread_ts=$THREAD_TS" >> $GITHUB_OUTPUT + echo "::notice::Created parent message with ts=$THREAD_TS" + + # Generate and post notes for each repo + generate: + name: Generate (${{ matrix.repo }}) + runs-on: ubuntu-latest + needs: [prepare, post-parent] + strategy: + matrix: ${{ fromJson(needs.prepare.outputs.matrix) }} + fail-fast: false + + container: + image: ghcr.io/block/goose:latest + options: --user root + env: + GOOSE_PROVIDER: openrouter + GOOSE_MODEL: anthropic/claude-sonnet-4 + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + HOME: /tmp/goose-home + + steps: + - name: Install dependencies + run: apt-get update && apt-get install -y git curl jq + + - name: Download last dispatch + uses: actions/download-artifact@v4 + with: + name: last-dispatch + path: /tmp + + - name: Clone repository + env: + REPO: ${{ matrix.repo }} + GH_TOKEN: ${{ secrets.ENTIRE_IO_PAT }} + run: | + git clone "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" /tmp/repo + cd /tmp/repo + git fetch --tags + + - name: Generate dispatch notes + env: + REPO: ${{ matrix.repo }} + LAST_DISPATCH_DATE: ${{ needs.prepare.outputs.last_dispatch_date }} + run: | + mkdir -p $HOME/.local/share/goose/sessions + mkdir -p $HOME/.config/goose + cd /tmp/repo + + cat > /tmp/recipe.yaml << 'RECIPE_EOF' + version: "1.0.0" + title: "Dispatch Notes Generator" + description: "Generate dispatch notes for changes since last dispatch" + + extensions: + - type: builtin + name: developer + + instructions: | + Generate dispatch notes for a repository, showing what's new since the last dispatch. + + Context: + - Repository: REPO_PLACEHOLDER + - Last Dispatch Date: DATE_PLACEHOLDER + - You are in the cloned repository directory + - The last dispatch content is at /tmp/last_dispatch.md + + Process: + 1. Read /tmp/last_dispatch.md to see what was already covered (releases mentioned, PRs linked) + 2. Get all releases since DATE_PLACEHOLDER: check git tags and their dates + 3. Get all merges to main/master since DATE_PLACEHOLDER: git log --since="DATE_PLACEHOLDER" --merges --first-parent main + 4. Also get non-merge commits to main since DATE_PLACEHOLDER: git log --since="DATE_PLACEHOLDER" --no-merges --first-parent main + 5. Cross-reference with the last dispatch - exclude anything already mentioned + + IMPORTANT: Use Slack mrkdwn format: + - *bold* (single asterisks) + - _italic_ (underscores) + - for links + - β€’ for bullet points + + Output Format - MUST start with a newline and use this EXACT structure: + + πŸ“¦ *Released `vX.Y.Z`* + β€’ Brief description of change () + β€’ Another change () + + πŸ”€ *Merged to main* + β€’ Change description () + β€’ Change description () + + If multiple releases, list each separately: + πŸ“¦ *Released `v1.2.0`* + β€’ Feature A (#101) + + πŸ“¦ *Released `v1.1.1`* + β€’ Bug fix B (#100) + + If nothing released, use: + πŸ“¦ *Released* _None_ + + If nothing merged, use: + πŸ”€ *Merged to main* _None_ + + IMPORTANT: Each section header (πŸ“¦ and πŸ”€) MUST be on its own line, not indented. + + Rules: + - ALWAYS include BOTH sections (Released AND Merged to main) - never skip a section + - Extract PR numbers from commit messages or merge commits + - Group released items under their release tag + - Keep descriptions concise + - Use the bullet hierarchy: β€’ for top level, β—¦ for second level, β€£ for third level + + Write ONLY the notes content to /tmp/dispatch_notes.md + + prompt: | + Generate dispatch notes for REPO_PLACEHOLDER showing changes since DATE_PLACEHOLDER. + Check /tmp/last_dispatch.md to avoid duplicating already-covered items. + RECIPE_EOF + + sed -i "s|REPO_PLACEHOLDER|${REPO}|g" /tmp/recipe.yaml + sed -i "s|DATE_PLACEHOLDER|${LAST_DISPATCH_DATE}|g" /tmp/recipe.yaml + goose run --recipe /tmp/recipe.yaml + + - name: Save notes for dispatch draft + env: + REPO: ${{ matrix.repo }} + run: | + # Create a safe filename from repo name + SAFE_NAME=$(echo "$REPO" | tr '/' '-') + cp /tmp/dispatch_notes.md "/tmp/notes-${SAFE_NAME}.md" + + - name: Upload repo notes + uses: actions/upload-artifact@v4 + with: + name: notes-${{ matrix.repo == 'entireio/cli' && 'entireio-cli' || matrix.repo == 'entirehq/entiredb' && 'entirehq-entiredb' || 'go-git-go-git' }} + path: /tmp/notes-*.md + retention-days: 1 + + - name: Post to Slack thread + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }} + THREAD_TS: ${{ needs.post-parent.outputs.thread_ts }} + REPO: ${{ matrix.repo }} + shell: bash + run: | + set -e + + if [ ! -f /tmp/dispatch_notes.md ]; then + echo "::error::Dispatch notes not generated" + exit 1 + fi + + NOTES=$(cat /tmp/dispatch_notes.md) + SAFE_LIMIT=2900 + + post_message() { + local text="$1" + + local payload + payload=$(jq -n \ + --arg channel "$SLACK_CHANNEL_ID" \ + --arg text "$text" \ + --arg ts "$THREAD_TS" \ + '{channel: $channel, text: $text, thread_ts: $ts, unfurl_links: false}') + + local response + response=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$payload") + + if [ "$(echo "$response" | jq -r '.ok')" != "true" ]; then + echo "::error::Slack API error: $(echo "$response" | jq -r '.error')" + return 1 + fi + } + + # Build the message with repo header + HEADER=$(printf 'πŸ“ *%s*\n' "$REPO") + # Ensure notes start with newline for proper separation + NOTES_CLEAN=$(echo "$NOTES" | sed 's/^[[:space:]]*//') + FULL_MSG=$(printf '%s\n%s' "$HEADER" "$NOTES_CLEAN") + + # Post in chunks if needed + if [ ${#FULL_MSG} -le $SAFE_LIMIT ]; then + post_message "$FULL_MSG" + else + # Post header first + post_message "$HEADER" + sleep 1 + + # Chunk the notes + echo "$NOTES" > /tmp/notes_full.txt + split -b $SAFE_LIMIT /tmp/notes_full.txt /tmp/chunk_ + + for chunk_file in /tmp/chunk_*; do + [ -f "$chunk_file" ] || continue + CHUNK=$(cat "$chunk_file") + [ -n "$CHUNK" ] && post_message "$CHUNK" + sleep 1 + done + fi + + echo "::notice::Posted dispatch notes to Slack thread" + + # Generate a draft dispatch post in Marvin's voice + draft-dispatch: + name: Draft Dispatch + runs-on: ubuntu-latest + needs: [prepare, post-parent, generate] + + container: + image: ghcr.io/block/goose:latest + options: --user root + env: + GOOSE_PROVIDER: openrouter + GOOSE_MODEL: anthropic/claude-sonnet-4 + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + HOME: /tmp/goose-home + + steps: + - name: Install dependencies + run: apt-get update && apt-get install -y git curl jq + + - name: Clone entire.io for dispatch examples + env: + GH_TOKEN: ${{ secrets.ENTIRE_IO_PAT }} + run: | + git clone --depth 1 "https://x-access-token:${GH_TOKEN}@github.com/entirehq/entire.io.git" /tmp/entire-io + + # Copy last 3 dispatches as style examples + DISPATCH_DIR="/tmp/entire-io/website/src/routes/_content/blog/-content" + ls -1 "$DISPATCH_DIR"/*dispatch*.md | sort | tail -3 | while read f; do + cp "$f" /tmp/ + done + + - name: Download all repo notes + uses: actions/download-artifact@v4 + with: + pattern: notes-* + path: /tmp/repo-notes + merge-multiple: true + + - name: Generate dispatch draft + env: + LAST_DISPATCH_DATE: ${{ needs.prepare.outputs.last_dispatch_date }} + run: | + mkdir -p $HOME/.local/share/goose/sessions + mkdir -p $HOME/.config/goose + + # Combine all repo notes into one file + echo "# Collected Notes from All Repos" > /tmp/all_notes.md + for f in /tmp/repo-notes/notes-*.md; do + if [ -f "$f" ]; then + REPO_NAME=$(basename "$f" .md | sed 's/notes-//' | tr '-' '/') + echo -e "\n## ${REPO_NAME}\n" >> /tmp/all_notes.md + cat "$f" >> /tmp/all_notes.md + fi + done + + cat > /tmp/recipe.yaml << 'RECIPE_EOF' + version: "1.0.0" + title: "Dispatch Draft Generator" + description: "Generate a dispatch blog post draft in Marvin's voice" + + extensions: + - type: builtin + name: developer + + instructions: | + Generate a draft dispatch blog post for the Entire project. + + Context: + - The collected notes from all repos are in /tmp/all_notes.md + - Example dispatches for voice/style reference are in /tmp/*.md (files like 2026-04-06-entire-dispatch-0x0008.md) + - This dispatch covers changes since DATE_PLACEHOLDER + + IMPORTANT: Only include items that are under "πŸ“¦ *Released*" sections in the notes. + Do NOT include items under "πŸ”€ *Merged to main*" - those are not released yet and + should not be in the dispatch. The dispatch only covers what has actually shipped + in tagged releases. + + Process: + 1. Read the example dispatch files in /tmp/ to understand: + - The voice (Marvin, a robot with personality - "Beep, boop", dry humor, references to having a brain the size of a planet) + - The structure (frontmatter, intro paragraph, sections with ## headings, closing) + - How features are described (user-focused, mentions contributors with @username) + 2. Read /tmp/all_notes.md and extract ONLY the "πŸ“¦ *Released*" items + 3. Ignore all "πŸ”€ *Merged to main*" items - they haven't shipped yet + 4. Determine the next dispatch number (look at the latest example and increment) + 5. Write a complete dispatch draft covering only released items + + Output Format (Markdown for the blog): + + ``` + --- + title: Entire Dispatch 0xNNNN + description: Brief comma-separated list of highlights + category: Dispatch + author: Marvin + --- + + Beep, boop. [Marvin intro with personality] + + [Intro paragraph summarizing the week's theme and highlights] + + ## [Feature/Section Name] + + - Description of the feature or change + - Sub-details if needed + - Thank contributors: Thank you to [@username](https://github.com/username)! + + ## [Another Section] + + ... + + ## Further Improvements + + [Group smaller changes here] + + [Closing paragraph in Marvin's voice] + ``` + + Rules: + - ONLY include released items (from "πŸ“¦ *Released*" sections) - never merged-but-unreleased + - Match Marvin's voice exactly - dry wit, robot references, occasionally sardonic + - Group related changes into logical sections with ## headings + - Mention release versions with links: [v0.5.3](https://github.com/entireio/cli/releases/tag/v0.5.3) + - Thank contributors by GitHub username when PRs show author + - Keep it concise but informative + - If nothing was released, write a short dispatch noting it's a quiet week + - End with a call to action (Discord, file issues, etc.) + + Write the complete dispatch draft to /tmp/dispatch_draft.md + + prompt: | + Generate a dispatch draft covering changes since DATE_PLACEHOLDER. + Read the example dispatches in /tmp/ for voice and style. + Read /tmp/all_notes.md for the content to include. + RECIPE_EOF + + sed -i "s|DATE_PLACEHOLDER|${LAST_DISPATCH_DATE}|g" /tmp/recipe.yaml + goose run --recipe /tmp/recipe.yaml + + - name: Post dispatch draft to Slack + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }} + THREAD_TS: ${{ needs.post-parent.outputs.thread_ts }} + shell: bash + run: | + set -e + + if [ ! -f /tmp/dispatch_draft.md ]; then + echo "::error::Dispatch draft not generated" + exit 1 + fi + + DRAFT=$(cat /tmp/dispatch_draft.md) + SAFE_LIMIT=2900 + + post_message() { + local text="$1" + + local payload + payload=$(jq -n \ + --arg channel "$SLACK_CHANNEL_ID" \ + --arg text "$text" \ + --arg ts "$THREAD_TS" \ + '{channel: $channel, text: $text, thread_ts: $ts, unfurl_links: false}') + + local response + response=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$payload") + + if [ "$(echo "$response" | jq -r '.ok')" != "true" ]; then + echo "::error::Slack API error: $(echo "$response" | jq -r '.error')" + return 1 + fi + } + + # Post header + post_message "πŸ“ *Draft Dispatch* (copy-paste ready)" + sleep 1 + + # Post draft in code block (chunk if needed) + # Wrap in code block for easy copying + CODE_BLOCK=$(printf '```\n%s\n```' "$DRAFT") + + BACKTICKS='```' + + if [ ${#CODE_BLOCK} -le $SAFE_LIMIT ]; then + post_message "$CODE_BLOCK" + else + # Split the draft itself and wrap each chunk + echo "$DRAFT" > /tmp/draft_full.txt + split -b 2700 /tmp/draft_full.txt /tmp/dchunk_ + + FIRST=true + for chunk_file in /tmp/dchunk_*; do + [ -f "$chunk_file" ] || continue + CHUNK=$(cat "$chunk_file") + if [ "$FIRST" = true ]; then + post_message "$(printf '%s\n%s' "$BACKTICKS" "$CHUNK")" + FIRST=false + else + post_message "${CHUNK}" + fi + sleep 1 + done + post_message "$BACKTICKS" + fi + + echo "::notice::Posted dispatch draft to Slack thread" From 095f35b8e4abed36125c3f3624a17541fc0a75dd Mon Sep 17 00:00:00 2001 From: goose-guest Date: Thu, 9 Apr 2026 14:57:17 -0700 Subject: [PATCH 2/2] test: add push trigger for testing on branch Signed-off-by: goose-guest --- .github/workflows/dispatch-notes-slack.yml | 7 +- .github/workflows/release-notes.yml | 575 ++++++++++++++++++ blog-post-enterprise-decision-makers.md | 315 ++++++++++ blog-post-how-entire-works.md | 421 +++++++++++++ docs/architecture/two-tier-storage-diagram.md | 104 ++++ 5 files changed, 1421 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release-notes.yml create mode 100644 blog-post-enterprise-decision-makers.md create mode 100644 blog-post-how-entire-works.md create mode 100644 docs/architecture/two-tier-storage-diagram.md diff --git a/.github/workflows/dispatch-notes-slack.yml b/.github/workflows/dispatch-notes-slack.yml index 11ee864cb7..fc2bae1d74 100644 --- a/.github/workflows/dispatch-notes-slack.yml +++ b/.github/workflows/dispatch-notes-slack.yml @@ -11,6 +11,11 @@ name: Dispatch Notes to Slack on: + # TEMPORARY: trigger on push to test branch (remove before merging to main) + push: + branches: + - test-dispatch-workflow + workflow_dispatch: inputs: repos: @@ -88,7 +93,7 @@ jobs: ALL_REPOS="entireio/cli entirehq/entiredb go-git/go-git" REPOS=() - if [ "$EVENT_NAME" = "schedule" ] || [ "$INPUT_REPOS" = "all" ]; then + if [ "$EVENT_NAME" = "schedule" ] || [ "$EVENT_NAME" = "push" ] || [ "$INPUT_REPOS" = "all" ]; then for repo in $ALL_REPOS; do REPOS+=("{\"repo\":\"${repo}\"}") done diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml new file mode 100644 index 0000000000..e43675005a --- /dev/null +++ b/.github/workflows/release-notes.yml @@ -0,0 +1,575 @@ +# Centralized Release Notes Generator +# +# Monitors multiple repos for new releases/deployments, generates AI-powered +# release notes using goose, and posts them to Slack. +# +# Repos tracked: +# - entireio/cli (semver tags, public) +# - go-git/go-git (semver tags, public) +# - entirehq/entire.io (push to main, private) +# - entirehq/entiredb (push to main, private) + +name: Release Notes + +on: + schedule: + # Daily at 09:00 UTC + - cron: "0 9 * * *" + + workflow_dispatch: + inputs: + repo: + description: "Repo to generate notes for (e.g. entireio/cli)" + required: true + type: choice + options: + - entireio/cli + - go-git/go-git + - entirehq/entire.io + - entirehq/entiredb + ref: + description: "Tag or commit SHA (leave empty for latest)" + required: false + type: string + +permissions: + contents: read + actions: write + +concurrency: + group: release-notes + cancel-in-progress: false + +jobs: + # --------------------------------------------------------------------------- + # 1. Detect which repos have new releases since last run + # --------------------------------------------------------------------------- + detect: + name: Detect new releases + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.build-matrix.outputs.matrix }} + has_releases: ${{ steps.build-matrix.outputs.has_releases }} + + steps: + - name: Download previous state + id: prev-state + uses: dawidd6/action-download-artifact@80620a5d27ce0ae443f0ee67f708b7f96eeef85e # v8 + with: + name: release-notes-state + path: /tmp/state + if_no_artifact_found: warn + workflow: release-notes.yml + continue-on-error: true + + - name: Initialize state + run: | + if [ ! -f /tmp/state/state.json ]; then + mkdir -p /tmp/state + echo '{}' > /tmp/state/state.json + fi + cat /tmp/state/state.json + + - name: Detect new releases + id: build-matrix + env: + GH_TOKEN: ${{ secrets.CROSS_ORG_PAT || secrets.GITHUB_TOKEN }} + MANUAL_REPO: ${{ inputs.repo }} + MANUAL_REF: ${{ inputs.ref }} + EVENT_NAME: ${{ github.event_name }} + run: | + STATE_FILE="/tmp/state/state.json" + MATRIX_ENTRIES="[]" + + check_tag_repo() { + local repo="$1" + local api_token="$2" + + # Get latest release tag + local latest_tag + latest_tag=$(curl -sf -H "Authorization: token ${api_token}" \ + "https://api.github.com/repos/${repo}/releases?per_page=1" \ + | jq -r '.[0].tag_name // empty') + + if [ -z "$latest_tag" ]; then + # Fallback: check latest semver tag via git ls-remote + latest_tag=$(curl -sf -H "Authorization: token ${api_token}" \ + "https://api.github.com/repos/${repo}/tags?per_page=1" \ + | jq -r '.[0].name // empty') + fi + + [ -z "$latest_tag" ] && return + + local prev_tag + prev_tag=$(jq -r --arg repo "$repo" '.[$repo].last_ref // empty' "$STATE_FILE") + + if [ "$latest_tag" != "$prev_tag" ]; then + echo "$latest_tag" + fi + } + + check_commit_repo() { + local repo="$1" + local api_token="$2" + + # Get latest commit on default branch + local latest_sha + latest_sha=$(curl -sf -H "Authorization: token ${api_token}" \ + "https://api.github.com/repos/${repo}/commits?per_page=1" \ + | jq -r '.[0].sha // empty') + + [ -z "$latest_sha" ] && return + + local prev_sha + prev_sha=$(jq -r --arg repo "$repo" '.[$repo].last_ref // empty' "$STATE_FILE") + + if [ "$latest_sha" != "$prev_sha" ]; then + echo "$latest_sha" + fi + } + + add_entry() { + local repo="$1" ref="$2" mode="$3" + MATRIX_ENTRIES=$(echo "$MATRIX_ENTRIES" | jq -c \ + --arg repo "$repo" --arg ref "$ref" --arg mode "$mode" \ + '. + [{"repo": $repo, "ref": $ref, "mode": $mode}]') + } + + if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ -n "$MANUAL_REPO" ]; then + # Manual trigger β€” single repo + case "$MANUAL_REPO" in + entireio/cli|go-git/go-git) + REF="$MANUAL_REF" + if [ -z "$REF" ]; then + REF=$(check_tag_repo "$MANUAL_REPO" "$GH_TOKEN") + [ -z "$REF" ] && REF=$(curl -sf -H "Authorization: token ${GH_TOKEN}" \ + "https://api.github.com/repos/${MANUAL_REPO}/releases?per_page=1" \ + | jq -r '.[0].tag_name // empty') + fi + [ -n "$REF" ] && add_entry "$MANUAL_REPO" "$REF" "tag" + ;; + entirehq/entire.io|entirehq/entiredb) + REF="$MANUAL_REF" + if [ -z "$REF" ]; then + REF=$(curl -sf -H "Authorization: token ${GH_TOKEN}" \ + "https://api.github.com/repos/${MANUAL_REPO}/commits?per_page=1" \ + | jq -r '.[0].sha // empty') + fi + [ -n "$REF" ] && add_entry "$MANUAL_REPO" "$REF" "commit" + ;; + esac + else + # Scheduled β€” check all repos + for repo in entireio/cli go-git/go-git; do + NEW_TAG=$(check_tag_repo "$repo" "$GH_TOKEN") + [ -n "$NEW_TAG" ] && add_entry "$repo" "$NEW_TAG" "tag" + done + + for repo in entirehq/entire.io entirehq/entiredb; do + NEW_SHA=$(check_commit_repo "$repo" "$GH_TOKEN") + [ -n "$NEW_SHA" ] && add_entry "$repo" "$NEW_SHA" "commit" + done + fi + + COUNT=$(echo "$MATRIX_ENTRIES" | jq 'length') + echo "Found $COUNT repo(s) with new releases" + echo "$MATRIX_ENTRIES" | jq . + + if [ "$COUNT" -gt 0 ]; then + echo "has_releases=true" >> "$GITHUB_OUTPUT" + echo "matrix={\"include\":$(echo "$MATRIX_ENTRIES" | jq -c .)}" >> "$GITHUB_OUTPUT" + else + echo "has_releases=false" >> "$GITHUB_OUTPUT" + echo "matrix={\"include\":[]}" >> "$GITHUB_OUTPUT" + fi + + # --------------------------------------------------------------------------- + # 2. Generate release notes for each detected repo + # --------------------------------------------------------------------------- + generate: + name: "Notes: ${{ matrix.repo }}" + runs-on: ubuntu-latest + needs: detect + if: needs.detect.outputs.has_releases == 'true' + + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.detect.outputs.matrix) }} + + container: + image: ghcr.io/block/goose:latest + options: --user root + env: + GOOSE_PROVIDER: ${{ vars.GOOSE_PROVIDER || 'anthropic' }} + GOOSE_MODEL: ${{ vars.GOOSE_MODEL || 'claude-sonnet-4-20250514' }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + HOME: /tmp/goose-home + + outputs: + # We collect per-repo outputs via artifacts since matrix outputs are tricky + notes_ready: "true" + + steps: + - name: Install tools + run: | + apt-get update && apt-get install -y gettext curl ripgrep git jq + + - name: Clone repository + env: + GH_TOKEN: ${{ secrets.CROSS_ORG_PAT || secrets.GITHUB_TOKEN }} + run: | + mkdir -p $HOME/.local/share/goose/sessions + mkdir -p $HOME/.config/goose + git config --global --add safe.directory /workspace + cd /tmp + git clone --filter=blob:none "https://x-access-token:${GH_TOKEN}@github.com/${{ matrix.repo }}.git" repo + cd repo + git fetch --tags + + - name: Determine commit range + id: range + working-directory: /tmp/repo + run: | + MODE="${{ matrix.mode }}" + REF="${{ matrix.ref }}" + REPO="${{ matrix.repo }}" + + if [ "$MODE" = "tag" ]; then + # Find previous tag + PREV_TAG=$(git describe --tags --abbrev=0 "${REF}^" 2>/dev/null || echo "") + if [ -n "$PREV_TAG" ]; then + echo "from=$PREV_TAG" >> "$GITHUB_OUTPUT" + echo "to=$REF" >> "$GITHUB_OUTPUT" + echo "range=${PREV_TAG}..${REF}" >> "$GITHUB_OUTPUT" + echo "label=${REF}" >> "$GITHUB_OUTPUT" + else + # No previous tag β€” use last 50 commits + echo "from=" >> "$GITHUB_OUTPUT" + echo "to=$REF" >> "$GITHUB_OUTPUT" + echo "range=$REF" >> "$GITHUB_OUTPUT" + echo "label=${REF}" >> "$GITHUB_OUTPUT" + fi + else + # Commit mode β€” get last 24h of commits on default branch + SINCE=$(date -u -d "24 hours ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-24H +%Y-%m-%dT%H:%M:%SZ) + COMMIT_COUNT=$(git log --since="$SINCE" --oneline --no-merges | wc -l) + if [ "$COMMIT_COUNT" -eq 0 ]; then + echo "No new commits in the last 24h for $REPO" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + SHORT_SHA=$(echo "$REF" | cut -c1-7) + echo "from=" >> "$GITHUB_OUTPUT" + echo "to=$REF" >> "$GITHUB_OUTPUT" + echo "range=--since=$SINCE" >> "$GITHUB_OUTPUT" + echo "label=main@${SHORT_SHA}" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + fi + + - name: Run goose + if: steps.range.outputs.skip != 'true' + working-directory: /tmp/repo + env: + RELEASE_LABEL: ${{ steps.range.outputs.label }} + COMMIT_RANGE: ${{ steps.range.outputs.range }} + FROM_REF: ${{ steps.range.outputs.from }} + TO_REF: ${{ steps.range.outputs.to }} + REPO_NAME: ${{ matrix.repo }} + RELEASE_MODE: ${{ matrix.mode }} + run: | + cat > /tmp/recipe.yaml << 'RECIPE' + version: "1.0.0" + title: "Release Notes Generator" + description: "Generate release notes" + + extensions: + - type: builtin + name: developer + + instructions: | + Generate release notes for a repository. You are already in the repo directory. + + Repository: ${REPO_NAME} + Release: ${RELEASE_LABEL} + Mode: ${RELEASE_MODE} + + ## Getting commits + If mode is "tag" and FROM_REF is set: + Run: git log ${FROM_REF}..${TO_REF} --oneline --no-merges + If mode is "tag" and FROM_REF is empty: + Run: git log ${TO_REF} --oneline --no-merges -50 + If mode is "commit": + Run: git log ${COMMIT_RANGE} --oneline --no-merges + + ## Output Format + Start with a header line: **${REPO_NAME}** β€” ${RELEASE_LABEL} + + Then categorize changes into these sections (skip empty sections): + - ✨ **Features** β€” New functionality + - πŸ› **Bug Fixes** β€” Bug fixes + - πŸ”§ **Improvements** β€” Enhancements to existing features + - πŸ“š **Documentation** β€” Documentation updates + + Format each item as: + - Concise description (#XXXX) + + Rules: + - Extract PR numbers from commit messages (look for (#XXXX) pattern) + - Keep descriptions concise and user-friendly + - The category headers convey intent, so don't repeat words like "Added", "Fixed" + - Skip merge commits and CI-only changes + - If there are very few changes, still categorize them + + ## Final Step + Write ONLY the release notes to /tmp/release_notes.md β€” no extra commentary. + + prompt: | + Generate release notes for ${REPO_NAME} release ${RELEASE_LABEL}. + RECIPE + + # Substitute env vars into recipe + envsubst '$REPO_NAME $RELEASE_LABEL $COMMIT_RANGE $FROM_REF $TO_REF $RELEASE_MODE' < /tmp/recipe.yaml > /tmp/recipe_final.yaml + + goose run --recipe /tmp/recipe_final.yaml + + - name: Prepare artifact + if: steps.range.outputs.skip != 'true' + run: | + mkdir -p /tmp/notes-output + SAFE_NAME=$(echo "${{ matrix.repo }}" | tr '/' '_') + + if [ -f /tmp/release_notes.md ]; then + cp /tmp/release_notes.md "/tmp/notes-output/${SAFE_NAME}.md" + + # Also save metadata + jq -n \ + --arg repo "${{ matrix.repo }}" \ + --arg ref "${{ matrix.ref }}" \ + --arg label "${{ steps.range.outputs.label }}" \ + --arg mode "${{ matrix.mode }}" \ + '{repo: $repo, ref: $ref, label: $label, mode: $mode}' \ + > "/tmp/notes-output/${SAFE_NAME}.json" + else + echo "::warning::No release notes generated for ${{ matrix.repo }}" + echo "No notable changes detected." > "/tmp/notes-output/${SAFE_NAME}.md" + fi + + - name: Upload notes + if: steps.range.outputs.skip != 'true' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: notes-${{ strategy.job-index }} + path: /tmp/notes-output/ + retention-days: 7 + + # --------------------------------------------------------------------------- + # 3. Post to Slack + # --------------------------------------------------------------------------- + post-to-slack: + name: Post to Slack + runs-on: ubuntu-latest + needs: [detect, generate] + if: needs.detect.outputs.has_releases == 'true' + + steps: + - name: Download all notes + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: notes-* + path: /tmp/all-notes + merge-multiple: true + + - name: Build Slack message + id: slack-msg + run: | + # Combine all notes into a thread-friendly format + HEADER=":rocket: *Release Notes β€” $(date -u +%Y-%m-%d)*" + + # Build the main message + echo "$HEADER" > /tmp/slack_main.txt + echo "" >> /tmp/slack_main.txt + + # List which repos have updates + for meta in /tmp/all-notes/*.json; do + [ -f "$meta" ] || continue + REPO=$(jq -r '.repo' "$meta") + LABEL=$(jq -r '.label' "$meta") + echo "β€’ *${REPO}* β€” \`${LABEL}\`" >> /tmp/slack_main.txt + done + + # Each repo's notes become a thread reply + THREAD_MESSAGES="[]" + for md in /tmp/all-notes/*.md; do + [ -f "$md" ] || continue + CONTENT=$(cat "$md") + THREAD_MESSAGES=$(echo "$THREAD_MESSAGES" | jq -c \ + --arg content "$CONTENT" \ + '. + [$content]') + done + + # Output + DELIMITER="EOF_$(openssl rand -hex 8)" + echo "main_msg<<$DELIMITER" >> "$GITHUB_OUTPUT" + cat /tmp/slack_main.txt >> "$GITHUB_OUTPUT" + echo "$DELIMITER" >> "$GITHUB_OUTPUT" + + echo "thread_messages<<$DELIMITER" >> "$GITHUB_OUTPUT" + echo "$THREAD_MESSAGES" >> "$GITHUB_OUTPUT" + echo "$DELIMITER" >> "$GITHUB_OUTPUT" + + - name: Post main message to Slack + id: slack-post + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }} + MAIN_MSG: ${{ steps.slack-msg.outputs.main_msg }} + run: | + if [ -z "$SLACK_BOT_TOKEN" ] || [ -z "$SLACK_CHANNEL_ID" ]; then + echo "::notice::Slack not configured β€” printing notes to log instead" + echo "---" + echo "$MAIN_MSG" + echo "---" + for md in /tmp/all-notes/*.md; do + [ -f "$md" ] || continue + echo "=== $(basename "$md") ===" + cat "$md" + echo "" + done + exit 0 + fi + + # Post main message + RESPONSE=$(curl -sf -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n \ + --arg channel "$SLACK_CHANNEL_ID" \ + --arg text "$MAIN_MSG" \ + '{channel: $channel, text: $text, unfurl_links: false}')") + + OK=$(echo "$RESPONSE" | jq -r '.ok') + if [ "$OK" != "true" ]; then + echo "::error::Slack API error: $(echo "$RESPONSE" | jq -c '.error')" + exit 1 + fi + + TS=$(echo "$RESPONSE" | jq -r '.ts') + echo "thread_ts=$TS" >> "$GITHUB_OUTPUT" + echo "Posted main message (ts=$TS)" + + - name: Post thread replies + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }} + THREAD_TS: ${{ steps.slack-post.outputs.thread_ts }} + if: steps.slack-post.outputs.thread_ts != '' + run: | + SLACK_LIMIT=3900 # Slack message limit is 4000, leave buffer + + for md in /tmp/all-notes/*.md; do + [ -f "$md" ] || continue + + CONTENT=$(cat "$md") + [ -z "$CONTENT" ] && continue + + # Split into chunks if needed + if [ ${#CONTENT} -le $SLACK_LIMIT ]; then + curl -sf -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n \ + --arg channel "$SLACK_CHANNEL_ID" \ + --arg text "$CONTENT" \ + --arg ts "$THREAD_TS" \ + '{channel: $channel, text: $text, thread_ts: $ts, unfurl_links: false}')" > /dev/null + else + # Split by lines into chunks + CHUNK="" + while IFS= read -r line || [ -n "$line" ]; do + if [ $(( ${#CHUNK} + ${#line} + 1 )) -gt $SLACK_LIMIT ] && [ -n "$CHUNK" ]; then + curl -sf -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n \ + --arg channel "$SLACK_CHANNEL_ID" \ + --arg text "$CHUNK" \ + --arg ts "$THREAD_TS" \ + '{channel: $channel, text: $text, thread_ts: $ts, unfurl_links: false}')" > /dev/null + sleep 1 + CHUNK="" + fi + CHUNK="${CHUNK}${line}\n" + done < "$md" + + # Send remaining chunk + if [ -n "$CHUNK" ]; then + curl -sf -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n \ + --arg channel "$SLACK_CHANNEL_ID" \ + --arg text "$CHUNK" \ + --arg ts "$THREAD_TS" \ + '{channel: $channel, text: $text, thread_ts: $ts, unfurl_links: false}')" > /dev/null + sleep 1 + fi + fi + + sleep 1 + done + + echo "::notice::Thread replies posted" + + # --------------------------------------------------------------------------- + # 4. Update state so we don't re-process the same releases + # --------------------------------------------------------------------------- + update-state: + name: Update state + runs-on: ubuntu-latest + needs: [detect, generate, post-to-slack] + if: always() && needs.generate.result == 'success' + + steps: + - name: Download previous state + uses: dawidd6/action-download-artifact@80620a5d27ce0ae443f0ee67f708b7f96eeef85e # v8 + with: + name: release-notes-state + path: /tmp/state + if_no_artifact_found: warn + workflow: release-notes.yml + continue-on-error: true + + - name: Download notes metadata + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: notes-* + path: /tmp/all-notes + merge-multiple: true + + - name: Update state file + run: | + STATE_FILE="/tmp/state/state.json" + [ -f "$STATE_FILE" ] || echo '{}' > "$STATE_FILE" + + for meta in /tmp/all-notes/*.json; do + [ -f "$meta" ] || continue + REPO=$(jq -r '.repo' "$meta") + REF=$(jq -r '.ref' "$meta") + TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) + + jq --arg repo "$REPO" --arg ref "$REF" --arg ts "$TIMESTAMP" \ + '.[$repo] = {last_ref: $ref, last_updated: $ts}' \ + "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" + done + + echo "Updated state:" + cat "$STATE_FILE" + + - name: Upload state + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: release-notes-state + path: /tmp/state/ + retention-days: 90 + overwrite: true diff --git a/blog-post-enterprise-decision-makers.md b/blog-post-enterprise-decision-makers.md new file mode 100644 index 0000000000..b7a1bd100d --- /dev/null +++ b/blog-post-enterprise-decision-makers.md @@ -0,0 +1,315 @@ +# AI Coding Assistants in the Enterprise: Why Session Traceability Matters + +*A strategic overview of AI development tooling governance and the case for git-native session tracking* + +--- + +AI coding assistants have moved from novelty to necessity. GitHub reports that Copilot users accept nearly 30% of code suggestions, and that number climbs higher with agentic tools like Claude Code and Cursor that can execute multi-step tasks autonomously. + +But as AI-assisted development scales across engineering organizations, a critical gap has emerged: **we have robust governance for code, but almost none for the AI interactions that produce it.** + +This creates real riskβ€”compliance gaps, audit failures, knowledge loss, and debugging blind spots. Entire is an open-source tool designed to close that gap by making AI sessions a first-class, auditable artifact in your git workflow. + +This post explains the problem, the technical approach, and the organizational benefits of adopting session traceability at scale. + +--- + +## The Governance Gap + +Modern software organizations have mature processes for code governance: + +| Artifact | Governance Controls | +|----------|-------------------| +| Source code | Version control, branch protection, code review | +| Dependencies | SBOMs, vulnerability scanning, license compliance | +| Builds | Reproducible pipelines, signed artifacts | +| Deployments | Change management, rollback procedures | + +But AI coding sessions? They exist in a gap: + +| Artifact | Current State | +|----------|--------------| +| AI prompts | Ephemeral, stored locally (if at all) | +| AI responses | Lost when session closes | +| AI-generated code | Merged into codebase with no provenance | +| Token usage | Unknown or aggregated at org level | + +This matters because: + +1. **Regulatory pressure is increasing** β€” The EU AI Act, emerging US state laws, and industry-specific regulations (HIPAA, SOX, PCI-DSS) increasingly require traceability for AI-assisted decisions. Code is a decision. + +2. **Audit requirements are expanding** β€” SOC 2, ISO 27001, and FedRAMP auditors are starting to ask: "How do you track AI-generated code?" Most organizations don't have a good answer. + +3. **Institutional knowledge is walking out the door** β€” When a developer leaves, their AI session historyβ€”often containing critical context about *why* code was writtenβ€”leaves with them. + +4. **Incident response is compromised** β€” When production breaks, teams can trace a bug to a commit. But if they can't trace the commit to the AI interaction that produced it, root cause analysis is incomplete. + +--- + +## The Technical Challenge + +Why hasn't this been solved? Because the obvious approaches have significant drawbacks: + +### Approach 1: Centralized Logging Platform + +Ship all AI interactions to a central service (Datadog, Splunk, custom solution). + +**Problems:** +- **Privacy concerns** β€” Prompts often contain proprietary code, customer data, or security-sensitive information +- **Data residency** β€” Logs may cross jurisdictional boundaries +- **Cost** β€” AI transcripts are large; logging costs scale poorly +- **Integration burden** β€” Each AI tool requires custom instrumentation +- **Availability dependency** β€” If the logging service is down, do you block development? + +### Approach 2: Agent-Native History + +Rely on the AI tool's built-in history (Claude's conversation history, Cursor's session storage). + +**Problems:** +- **Vendor lock-in** β€” History format varies by tool; switching tools means losing history +- **Local-only** β€” Most agent history is stored on the developer's machine, not shareable +- **No commit linkage** β€” No way to connect a conversation to the code it produced +- **Retention uncertainty** β€” Vendors may change retention policies without notice + +### Approach 3: Manual Documentation + +Ask developers to document AI usage in commit messages or wikis. + +**Problems:** +- **Compliance theater** β€” Developers won't do it consistently under deadline pressure +- **Incomplete capture** β€” Manual summaries miss the full context +- **No enforcement** β€” No technical control to ensure documentation happens + +--- + +## Entire's Approach: Git-Native Session Storage + +Entire takes a different approach: **store AI session data in git itself, linked to the commits that session produced.** + +This means: + +- **No external services** β€” Data never leaves your infrastructure +- **Automatic capture** β€” Hooks fire on agent events; developers don't change their workflow +- **Commit-level linkage** β€” Every commit can be traced to its AI session (prompts, responses, files touched) +- **Standard tooling** β€” Session data is accessible via `git log`, `git diff`, and standard git hosting + +### How It Works + +1. **Agent hooks capture events** β€” When a developer uses Claude Code, Cursor, or another supported agent, lifecycle hooks notify Entire of session start, prompts, and completion. + +2. **Checkpoints track state** β€” As the agent works, Entire creates checkpoints containing the transcript and modified files. These are stored on temporary "shadow branches" that don't pollute the main history. + +3. **Commits trigger condensation** β€” When the developer commits, Entire links the session metadata to that commit via a trailer (`Entire-Checkpoint: `) and moves the data to a permanent metadata branch. + +4. **Metadata travels with code** β€” When branches are pushed, the session metadata branch can be pushed alongside. Teammates, auditors, and future developers can access the full context. + +``` +Developer's Branch Metadata Branch (entire/checkpoints/v1) + β”‚ β”‚ + β–Ό β”‚ +[Feature Commit] β”‚ + β”‚ β”‚ + β”‚ Entire-Checkpoint: a3b2c4d5e6f7 β”‚ + β”‚ β”‚ β”‚ + β”‚ └──────────────────────────────────┼──▢ a3/b2c4d5e6f7/ + β”‚ β”‚ β”œβ”€β”€ metadata.json + β–Ό β”‚ β”œβ”€β”€ full.jsonl (transcript) +[Next Commit] β”‚ └── prompt.txt + β–Ό +``` + +--- + +## Enterprise Benefits + +### 1. Compliance & Audit Readiness + +**The problem:** Auditors ask "How do you ensure AI-generated code is reviewed?" You show them your code review process. They ask "How do you know which code was AI-generated?" Silence. + +**With Entire:** Every commit linked to an AI session has a `Entire-Checkpoint` trailer. You can: + +- Query which commits involved AI assistance +- Show the exact prompts and responses for any commit +- Demonstrate that AI-generated code went through standard review processes +- Prove token-level attribution (which lines came from AI vs. human edits) + +**Audit response time:** Instead of "we'd need to investigate," you can answer in minutes. + +### 2. Intellectual Property Protection + +**The problem:** AI-generated code has uncertain copyright status. If you can't identify which code was AI-assisted, you can't assess IP risk. + +**With Entire:** Session metadata includes: + +- Exact prompts provided to the AI (your IP) +- Token-level attribution showing AI vs. human contribution +- Model identification (which AI system generated the code) + +This supports IP audits, licensing decisions, and potential future regulatory requirements around AI content labeling. + +### 3. Knowledge Retention & Onboarding + +**The problem:** A senior engineer leaves. Their commits remain, but the *reasoning* behind those commitsβ€”captured in AI sessionsβ€”is gone. + +**With Entire:** The full session transcript is preserved: + +``` +$ entire explain abc1234 + +Commit: abc1234 (Optimize payment processing pipeline) +Session: 2026-02-15-def789 +Developer: jane.smith@company.com + +Prompt: "The payment processing is timing out for large batches. +Can you analyze the bottleneck and propose optimizations?" + +AI Analysis: +- Identified N+1 query pattern in batch processor +- Recommended connection pooling configuration +- Suggested index on transactions.merchant_id + +Files modified: +- src/payments/batch_processor.go +- config/database.yml +- migrations/20260215_add_merchant_index.sql + +Token usage: 15,230 input, 4,102 output +``` + +New team members can understand not just *what* changed but *why*β€”the original problem, the analysis, and the reasoning. + +### 4. Incident Response & Root Cause Analysis + +**The problem:** Production incident. You trace the bug to a commit from 3 months ago. The developer who wrote it is on vacation. The commit message says "Refactor authentication flow." + +**With Entire:** You can see the exact AI interaction: + +- What problem was the developer trying to solve? +- What did they ask the AI to do? +- What alternatives were considered? +- Were there any warnings in the AI response that were ignored? + +This accelerates root cause analysis and informs prevention strategies. + +### 5. Cost Visibility & Optimization + +**The problem:** Your AI tooling bill is growing, but you don't know which teams, projects, or use cases are driving costs. + +**With Entire:** Token usage is captured per-session and linked to commits: + +- Aggregate by repository, team, or project +- Identify high-token-usage patterns (inefficient prompting, unnecessary context) +- Correlate AI investment with code output +- Set baselines and detect anomalies + +--- + +## Deployment Considerations + +### Security & Privacy + +**Data residency:** All session data is stored in git. If your repositories are on-premises or in a specific cloud region, session data follows the same residency. + +**Access control:** Session metadata inherits git's access control. If a developer can read a branch, they can read linked session data. If they can't, they can't. + +**Sensitive data handling:** Entire includes configurable PII redaction. Detected secrets (API keys, tokens) are automatically redacted before storage. Custom patterns can be added. + +**No external transmission:** Entire is a local CLI tool. Session data is never sent to external services (unless you explicitly push the metadata branch to a remote). + +### Integration + +**Supported agents:** Claude Code, Cursor, Gemini CLI, OpenCode, GitHub Copilot CLI, Factory AI Droid. Additional agents can be added via a plugin interface. + +**Git hosting:** Works with any git remoteβ€”GitHub, GitLab, Bitbucket, Azure DevOps, self-hosted. + +**CI/CD:** Session metadata can be queried in pipelines for compliance gates, reporting, or alerting. + +### Rollout Strategy + +**Phase 1: Pilot** β€” Enable on a single team or repository. Validate that session capture works with your agent configuration. Review captured data for sensitive content. + +**Phase 2: Policy** β€” Define retention policies (how long to keep session data), access policies (who can view session metadata), and redaction rules (what to filter). + +**Phase 3: Expand** β€” Roll out to additional teams. Integrate with compliance reporting. Train developers on `entire status`, `entire explain`, and `entire rewind`. + +**Phase 4: Audit** β€” Use session metadata in internal/external audits. Establish baseline metrics for AI usage. + +--- + +## Comparison: Build vs. Buy vs. Open Source + +| Approach | Pros | Cons | +|----------|------|------| +| **Build internal tooling** | Full control, custom requirements | Engineering investment, maintenance burden, agent integration complexity | +| **Buy vendor solution** | Managed service, support | Data leaves your infrastructure, vendor lock-in, cost at scale | +| **Entire (open source)** | Git-native (no external services), multi-agent support, community-driven | Self-managed, community support model | + +Entire is particularly well-suited for organizations that: + +- Have strict data residency requirements +- Use multiple AI coding assistants +- Want to avoid vendor lock-in +- Have engineering capacity to manage open-source tooling + +--- + +## Frequently Asked Questions + +**Q: Does Entire slow down development?** + +A: No. Hooks execute asynchronously and are designed to fail silently. If anything goes wrong, the developer's workflow continues uninterrupted. Checkpoint creation adds ~50-100ms per agent turn, which is imperceptible. + +**Q: What if developers don't want their sessions tracked?** + +A: Entire is opt-out at the commit level. Developers can remove the `Entire-Checkpoint` trailer from any commit message to skip session linking. Organizations can set policies on when this is acceptable. + +**Q: How much storage does session data consume?** + +A: Session transcripts are typically 10-100KB per commit. For a repository with 1,000 commits/year, expect 10-100MB of session metadataβ€”negligible compared to code history. + +**Q: Can we query session data programmatically?** + +A: Yes. Session data is stored as JSON in git trees. You can read it with standard git commands (`git show entire/checkpoints/v1:a3/b2c4d5e6f7/metadata.json`) or via Entire's CLI (`entire explain `). + +**Q: What about sessions that don't result in commits?** + +A: Exploratory sessions (questions, research) that don't produce code changes are captured on temporary shadow branches. These are retained until the developer commits or explicitly cleans them up. Retention policy is configurable. + +--- + +## Getting Started + +For a pilot deployment: + +```bash +# Install via Homebrew (macOS/Linux) +brew install entireio/tap/entire + +# Or via Go +go install github.com/entireio/cli/cmd/entire@latest + +# Enable in a repository +cd your-repository +entire enable + +# Verify setup +entire status +``` + +For enterprise deployment guidance, custom integration requirements, or to discuss your specific compliance needs, contact the Entire team or open a discussion on the [GitHub repository](https://github.com/entireio/cli). + +--- + +## Conclusion + +AI coding assistants are here to stay. The productivity gains are too significant to ignore. But as AI-assisted development scales, so does the governance gap. + +The question isn't whether to track AI sessionsβ€”it's how. Centralized logging has privacy and cost challenges. Vendor-native history has lock-in and linkage problems. Manual documentation doesn't work at scale. + +Git-native session tracking offers a third path: automatic capture, commit-level linkage, standard tooling, and data that stays in your infrastructure. + +For organizations serious about AI governance, the time to establish session traceability is nowβ€”before auditors start asking questions you can't answer. + +--- + +*Entire is open source under the MIT license. Visit [github.com/entireio/cli](https://github.com/entireio/cli) to learn more.* diff --git a/blog-post-how-entire-works.md b/blog-post-how-entire-works.md new file mode 100644 index 0000000000..64a60606a8 --- /dev/null +++ b/blog-post-how-entire-works.md @@ -0,0 +1,421 @@ +# How Entire Works: Building a Time Machine for AI Coding Sessions + +*A deep dive into the architecture behind git-native AI session tracking* + +--- + +If you've ever used an AI coding assistant like Claude Code, Cursor, or GitHub Copilot, you've probably experienced this: you're three prompts deep into a refactoring session, the agent makes a change that breaks everything, and suddenly you're playing detectiveβ€”trying to remember what worked, what didn't, and how to get back to that moment 15 minutes ago when things were fine. + +Entire is a CLI tool that solves this problem. It hooks into your git workflow to capture AI agent sessions as you work, creating checkpoints you can rewind to at any moment. Think of it as Time Machine for your AI coding sessions. + +But how does it actually work? Let's pop the hood. + +--- + +## The Problem: AI Sessions Are Ephemeral + +When you work with an AI coding assistant, a lot happens: + +- You submit prompts describing what you want +- The agent reads files, reasons about your codebase, and makes changes +- You iterate, refine, and eventually commit + +But once you close that session, most of that context evaporates. The prompts, the reasoning, the intermediate statesβ€”gone. If you need to understand *why* code changed (not just *what* changed), you're left reconstructing it from memory. + +This creates real problems: + +1. **Recovery is painful** β€” When an agent goes off the rails, undoing its work means manual `git checkout` commands and hoping you remember which files changed +2. **Onboarding is harder** β€” New team members see commits but not the intent behind them +3. **Auditing is incomplete** β€” You can trace code to commits, but not to the AI interactions that produced them + +Entire addresses all three by linking your git commits to the full session transcriptβ€”prompts, responses, files touched, and token usage. + +--- + +## The Core Insight: Git Is Already a Time Machine + +Here's the key architectural insight: **git is already a content-addressed storage system with branching, merging, and history traversal built in**. Rather than build a separate database, Entire stores everything in git itself. + +This means: + +- **No external services** β€” Everything lives in your repository +- **Collaboration works automatically** β€” Push your branch, and teammates get your session history +- **Existing tools work** β€” `git log`, `git diff`, and your IDE all understand the data + +The challenge is doing this without polluting your commit history. Nobody wants their clean `main` branch cluttered with "AI checkpoint: step 47" commits. + +--- + +## Architecture Overview + +Entire has four main layers: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ENTIRE CLI β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ Agents Lifecycle Strategy Storage β”‚ +β”‚ (Claude, Dispatcher (Manual- (Git β”‚ +β”‚ Gemini, (Events) Commit) Trees) β”‚ +β”‚ Cursor) β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +Let's walk through each one. + +--- + +## Layer 1: The Agent Abstraction + +Entire supports multiple AI coding assistants: Claude Code, Gemini CLI, Cursor, OpenCode, GitHub Copilot CLI, and Factory AI Droid. Each has its own hook format, transcript structure, and session storage. + +Rather than writing separate logic for each, Entire defines a common `Agent` interface: + +```go +type Agent interface { + Name() string // "claude-code", "cursor" + Type() string // "Claude Code", "Cursor" + DetectPresence(ctx) (bool, error) // Is this agent configured? + ReadTranscript(sessionRef) ([]byte, error) // Read the session log + // ... more methods +} +``` + +Each agent implements this interface by translating its native format to Entire's normalized model. For example, Claude Code stores transcripts as JSONL files in `.claude/projects/`, while Cursor uses a different structure in `.cursor/`. + +This abstraction means the rest of the system doesn't care which agent you're usingβ€”it just works with normalized events and transcripts. + +### Lifecycle Hooks + +Agents that support hooks (most do) implement a second interface: + +```go +type HookSupport interface { + Agent + HookNames() []string // ["session-start", "stop", ...] + ParseHookEvent(ctx, hookName, stdin) (*Event, error) // Translate to normalized event + InstallHooks(ctx, localDev, force) (int, error) // Write hook config +} +``` + +When you run `entire enable --agent claude-code`, Entire writes to `.claude/settings.json`: + +```json +{ + "hooks": { + "SessionStart": [{"command": "entire hooks claude-code session-start"}], + "Stop": [{"command": "entire hooks claude-code stop"}], + "UserPromptSubmit": [{"command": "entire hooks claude-code user-prompt-submit"}] + } +} +``` + +Now whenever Claude Code starts, stops, or receives a prompt, it invokes Entire. The agent doesn't know or care what Entire doesβ€”it just fires the hook and continues. + +--- + +## Layer 2: The Lifecycle Dispatcher + +When a hook fires, Entire needs to decide what to do. This is the lifecycle dispatcher's job. + +First, it parses the agent-specific input into a normalized event: + +```go +type Event struct { + Type EventType // SessionStart, TurnStart, TurnEnd, etc. + SessionID string // "2026-03-19-abc123..." + SessionRef string // Path to transcript file + Prompt string // User's prompt (for TurnStart) + Timestamp time.Time +} +``` + +Then it routes the event to the appropriate handler: + +```go +func DispatchLifecycleEvent(ctx, agent, event) error { + switch event.Type { + case SessionStart: + return handleLifecycleSessionStart(ctx, agent, event) + case TurnStart: + return handleLifecycleTurnStart(ctx, agent, event) + case TurnEnd: + return handleLifecycleTurnEnd(ctx, agent, event) + // ... + } +} +``` + +Each handler performs bookkeeping: updating session state, recording file changes, creating checkpoints. + +### The State Machine + +Sessions have a lifecycle: they start, the user submits prompts, the agent works, and eventually the session ends. Entire models this as a state machine: + +``` + TurnStart TurnEnd + β”‚ β”‚ + β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ IDLE β”‚ ◀───▢ β”‚ ACTIVE β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ SessionStop + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ ENDED β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +The interesting part is what happens on `GitCommit`. If you commit while a session is active (or idle with uncommitted checkpoints), the state machine triggers a "condense" actionβ€”more on that below. + +--- + +## Layer 3: The Manual-Commit Strategy + +This is where the magic happens. The strategy determines *how* checkpoints are stored and *when* they're made permanent. + +Entire uses a "manual-commit" strategy with two key principles: + +1. **Never create commits on the user's branch** β€” Your commit history stays clean +2. **Store checkpoints on shadow branches** β€” Temporary state that disappears after you commit + +### Shadow Branches + +When you're working with an AI agent, Entire creates a shadow branch named after your current commit: + +``` +entire/abc1234-def567 + ─────── ────── + β”‚ β”‚ + β”‚ └── Worktree hash (for concurrent worktrees) + └── First 7 chars of HEAD commit +``` + +Every time the agent finishes a turn, Entire writes a checkpoint to this branch: + +``` +Shadow branch tree: +β”œβ”€β”€ +└── .entire/ + └── metadata/ + └── 2026-03-19-session-id/ + β”œβ”€β”€ full.jsonl # Complete transcript + └── prompt.txt # User prompts +``` + +This branch exists only in your local `.git`β€”it's never pushed. It's a scratch space for in-progress work. + +### Condensation: Shadow β†’ Permanent + +When you run `git commit`, something interesting happens. Entire installs git hooks that intercept the commit process: + +**prepare-commit-msg**: Before you edit the commit message, Entire adds a trailer: + +``` +Implement user authentication + +Entire-Checkpoint: a3b2c4d5e6f7 +``` + +This 12-character ID is the stable link between your commit and the session metadata. + +**post-commit**: After git creates the commit, Entire "condenses" the shadow branch: + +1. Read the transcript and metadata from the shadow branch +2. Calculate attribution (which lines came from the agent vs. human edits) +3. Write everything to a permanent branch: `entire/checkpoints/v1` +4. Delete the shadow branch + +The permanent branch uses a sharded structure to avoid directory bloat: + +``` +entire/checkpoints/v1 +β”œβ”€β”€ a3/ +β”‚ └── b2c4d5e6f7/ # Checkpoint a3b2c4d5e6f7 +β”‚ β”œβ”€β”€ metadata.json # Summary stats +β”‚ └── 0/ # Session data +β”‚ β”œβ”€β”€ full.jsonl # Transcript +β”‚ β”œβ”€β”€ prompt.txt # Prompts +β”‚ └── metadata.json # Token usage, attribution +``` + +Now you have bidirectional linking: + +- **Commit β†’ Metadata**: Parse the `Entire-Checkpoint` trailer, look up `a3/b2c4d5e6f7/` +- **Metadata β†’ Commit**: Search branch history for commits with that trailer + +--- + +## Layer 4: Git Tree Building + +Here's where things get technically interesting. Entire needs to create git commits containing both your code *and* metadata, without ever touching your working directory. + +It does this using go-git's plumbing APIs to build trees in memory: + +```go +func (s *GitStore) WriteTemporary(ctx, opts) (WriteTemporaryResult, error) { + // 1. Get the base tree (from HEAD) + baseTreeHash := getHeadTree() + + // 2. Build a new tree with modifications + treeHash := s.buildTreeWithChanges(ctx, baseTreeHash, + opts.ModifiedFiles, // Files the agent changed + opts.DeletedFiles, // Files the agent removed + opts.MetadataDir, // ".entire/metadata/" + opts.MetadataDirAbs, // Absolute path to read metadata from + ) + + // 3. Create a commit pointing to this tree + commitHash := s.createCommit(treeHash, parentHash, commitMsg, ...) + + // 4. Update the branch reference + s.repo.Storer.SetReference(plumbing.NewHashReference(refName, commitHash)) + + return WriteTemporaryResult{CommitHash: commitHash}, nil +} +``` + +The key insight: **git trees are content-addressed**. If you build a tree with the same contents, you get the same hash. This enables efficient deduplicationβ€”if nothing changed since the last checkpoint, Entire skips creating a new commit. + +--- + +## Rewind: Restoring Previous States + +All this machinery exists to enable one killer feature: **rewind**. + +When you run `entire rewind`, Entire shows you available checkpoints: + +``` +$ entire rewind + +Select a checkpoint to rewind to: +> a3b2c4d (2026-03-19 14:30) Implement login endpoint + (2026-03-19 14:25) Add user model + (2026-03-19 14:20) Set up database connection +``` + +When you select one, Entire: + +1. **Reads the checkpoint tree** from the shadow branch (uncommitted) or metadata branch (committed) +2. **Calculates the diff** between that tree and your current working directory +3. **Restores files** from the checkpoint +4. **Deletes orphaned files** (files created after the checkpoint) +5. **Truncates the transcript** so the agent's context matches the restored state + +That last point is crucial. If you rewind to step 3, you don't want the agent's context to include steps 4 and 5β€”that would be confusing. Entire truncates the transcript to maintain consistency. + +--- + +## Concurrent Sessions & Worktrees + +Real development isn't always linear. You might: + +- Have two terminal windows working on different features +- Use git worktrees for parallel development +- Start a new session before the old one finished + +Entire handles all of these. + +**Concurrent sessions** on the same commit share a shadow branch. Each session's checkpoints are tagged with their session ID, so rewind knows which checkpoint belongs to which session. + +**Worktrees** get their own shadow branch namespace. The worktree hash in `entire/abc1234-def567` ensures sessions in different worktrees don't collide. + +**Shadow branch migration**: If you do `git stash && git pull && git stash pop` (changing HEAD without committing), the shadow branch is automatically renamed to track the new base commit. + +--- + +## The Value Proposition + +So why does all this matter? What can you actually *do* with Entire? + +### 1. Fearless Experimentation + +With traditional version control, you commit when you're confident code works. With Entire, you can ask the AI to try risky refactors knowing you can rewind in seconds if it goes wrong. + +This changes how you work. Instead of carefully constraining prompts to avoid mistakes, you can be ambitious: "Refactor this entire module to use the new API." If it works, great. If not, rewind and try a different approach. + +### 2. Understanding Code History + +Git blame tells you *who* changed a line and *when*. Entire tells you *why*: + +``` +$ entire explain abc1234 + +Commit: abc1234 +Session: 2026-03-19-def456 + +Prompt: "The login endpoint is timing out under load. Can you optimize +the database queries?" + +Summary: +- Added database connection pooling +- Replaced N+1 query with JOIN +- Added index on users.email + +Files: src/auth/login.go, migrations/add_email_index.sql +Tokens: 12,450 input, 3,200 output +``` + +This is invaluable for: + +- **Code review**: Understanding the intent behind changes +- **Debugging**: Tracing a bug back to the prompt that introduced it +- **Onboarding**: Showing new team members how features were built + +### 3. Collaboration & Handoffs + +Sessions are stored in git, which means they're shareable: + +``` +$ entire resume feature-branch + +Restoring session from feature-branch... +To continue: claude "Let's pick up where we left off" +``` + +Your teammate can literally resume your AI session, with full context of what was tried, what worked, and what didn't. + +### 4. Audit & Compliance + +For regulated industries, being able to trace code back to specific AI interactions is increasingly important. Entire provides that audit trail automaticallyβ€”no extra process needed. + +--- + +## Design Philosophy + +A few principles guided Entire's design: + +**Git-native**: Everything is stored in git. No external databases, no cloud dependencies. This makes it robust, portable, and familiar. + +**Non-invasive**: Your commit history stays clean. Entire never creates commits on your working branch without your explicit action. + +**Agent-agnostic**: The same architecture works with any AI coding assistant. Add a new agent by implementing an interfaceβ€”the strategy and storage layers don't change. + +**Opt-out friendly**: Don't want a commit linked to session data? Just delete the `Entire-Checkpoint` trailer before committing. Entire respects your choice. + +**Fail-safe**: If anything goes wrong, Entire fails silently rather than blocking your work. Git hooks return success even on errorsβ€”your commits always go through. + +--- + +## Conclusion + +Entire exists because AI coding assistants are genuinely useful, but their ephemeral nature creates real friction. By treating session data as a first-class artifactβ€”versioned, linked to commits, and rewindableβ€”it removes that friction. + +The technical approach is deliberately conservative: use git's existing primitives, don't modify the user's working tree, store everything as content-addressed blobs. This makes the system predictable and debuggable. + +If you're using AI assistants for serious development work, give Entire a try: + +```bash +brew install entireio/tap/entire +cd your-project && entire enable +``` + +Your future selfβ€”the one trying to understand why the code looks the way it doesβ€”will thank you. + +--- + +*Entire is open source. Check out the [GitHub repository](https://github.com/entireio/cli) to learn more or contribute.* diff --git a/docs/architecture/two-tier-storage-diagram.md b/docs/architecture/two-tier-storage-diagram.md new file mode 100644 index 0000000000..aa93b14932 --- /dev/null +++ b/docs/architecture/two-tier-storage-diagram.md @@ -0,0 +1,104 @@ +# Two-Tier Storage Model + +## Overview + +Entire uses a two-tier storage model to balance mid-session rewind capability with clean commit history. + +## The Flow + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ WHILE YOU WORK β”‚ +β”‚ ══════════════ β”‚ +β”‚ β”‚ +β”‚ Your branch: A ─── B ─── C (HEAD) ◄── no checkpoint commits here β”‚ +β”‚ β”‚ +β”‚ β”‚ +β”‚ Shadow branch: entire/c4f8a2b-d3e1a9 ◄── out-of-band scratch space β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ checkpoint 1 β”‚ ◄── rewindable β”‚ +β”‚ β”‚ checkpoint 2 β”‚ ◄── rewindable β”‚ +β”‚ β”‚ checkpoint 3 β”‚ ◄── rewindable β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ (full code snapshots + transcripts) β”‚ +β”‚ β”‚ +β”‚ β€’ Rewind anytime if agent goes sideways β”‚ +β”‚ β€’ Never appears in git log or PRs β”‚ +β”‚ β€’ Purely scratch space for the session β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + β”‚ + β”‚ git commit + β–Ό + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ONCE YOU COMMIT β”‚ +β”‚ ══════════════ β”‚ +β”‚ β”‚ +β”‚ Your branch: A ─── B ─── C ─── D (HEAD) β”‚ +β”‚ β”‚ β”‚ +β”‚ └── Entire-Checkpoint: a3b2c4d5e6f7 β”‚ +β”‚ (trailer links to permanent record) β”‚ +β”‚ β”‚ +β”‚ β”‚ +β”‚ Shadow branch: (deleted) β”‚ +β”‚ β”‚ +β”‚ β”‚ +β”‚ Checkpoints branch: entire/checkpoints/v1 β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ a3/b2c4d5e6f7/ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ metadata.json β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ full.jsonl β”‚ ◄── transcript β”‚ +β”‚ β”‚ β”œβ”€β”€ prompt.txt β”‚ ◄── user prompts β”‚ +β”‚ β”‚ └── (token usage, β”‚ β”‚ +β”‚ β”‚ attribution, β”‚ β”‚ +β”‚ β”‚ summary...) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β€’ Temporary state condensed into permanent record β”‚ +β”‚ β€’ Shadow branch cleaned up β”‚ +β”‚ β€’ Your commit links to the checkpoint via trailer β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Shadow Branch Naming + +Shadow branches follow a specific naming convention to stay isolated and organized: + +``` +entire/- + ─────┬───── ──────┬────── + β”‚ β”‚ + β”‚ └─ 6-char hash of worktree path + β”‚ (allows concurrent worktrees) + β”‚ + └─ 7-char prefix of base commit + (ties checkpoints to your HEAD) + +Example: entire/c4f8a2b-d3e1a9 +``` + +## What Gets Stored Where + +| Data | Shadow Branch (pre-commit) | Checkpoints Branch (post-commit) | +|------|----------------------------|----------------------------------| +| Code snapshots | βœ“ Full worktree | βœ— | +| Transcript | βœ“ | βœ“ | +| User prompts | βœ“ | βœ“ | +| Token usage | βœ— | βœ“ | +| File attribution | βœ— | βœ“ | +| AI-generated summary | βœ— | βœ“ | + +## Key Properties + +**Shadow branches (while you work):** +- Enable mid-session rewind without polluting your branch +- Completely invisible to normal git workflows +- Automatically cleaned up after commit +- Named per base-commit + worktree for isolation + +**Checkpoints branch (after you commit):** +- Permanent, auditable record of AI sessions +- Linked to your commits via `Entire-Checkpoint` trailer +- Can be pushed to remote for team visibility +- Sharded storage (256 directories) for scale