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
44 changes: 44 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Nightly Release

on:
schedule:
- cron: "0 6 * * *" # 06:00 UTC daily
workflow_dispatch:

permissions:
contents: write

concurrency:
group: nightly-release-${{ github.ref }}
cancel-in-progress: true
jobs:
create-nightly-tag:
runs-on: ubuntu-latest
steps:
- name: Generate token
id: app-token
uses: actions/create-github-app-token@v3.0.0
with:
app-id: ${{ secrets.HOMEBREW_TAP_APP_ID }}
private-key: ${{ secrets.HOMEBREW_TAP_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: cli

- uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}

- name: Create nightly tag
run: |
TAG=$(scripts/create-nightly-tag.sh)
EXIT_CODE=$?
if [ "$EXIT_CODE" -eq 2 ]; then
echo "Skipping — no new nightly needed."
exit 0
elif [ "$EXIT_CODE" -ne 0 ] || [ -z "$TAG" ]; then
echo "::error::Failed to generate nightly tag"
exit 1
fi
git tag "$TAG"
git push origin "$TAG"
28 changes: 28 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,18 @@ jobs:
homebrew-tap
scoop-bucket

- name: Detect release type
id: release-type
run: |
VERSION="${GITHUB_REF_NAME#v}"
if [[ "$VERSION" == *-* ]]; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi

- name: Extract release notes from CHANGELOG.md
if: steps.release-type.outputs.prerelease == 'false'
run: |
VERSION="${GITHUB_REF_NAME#v}"
awk -v ver="$VERSION" 'BEGIN{header="^## \\[" ver "\\]"} $0 ~ header{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md > "$RUNNER_TEMP/release_notes.md"
Expand All @@ -39,6 +50,23 @@ jobs:
exit 1
fi

- name: Generate nightly release notes
if: steps.release-type.outputs.prerelease == 'true'
run: |
LAST_NIGHTLY=$(git tag -l 'v*-nightly.*' --sort=-creatordate | grep -v "^${GITHUB_REF_NAME}$" | head -1)
if [ -n "$LAST_NIGHTLY" ]; then
BASE_TAG="$LAST_NIGHTLY"
else
BASE_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude '*-*' HEAD 2>/dev/null || echo "")
fi
echo "## Nightly Build (${GITHUB_REF_NAME})" > "$RUNNER_TEMP/release_notes.md"
echo "" >> "$RUNNER_TEMP/release_notes.md"
if [ -n "$BASE_TAG" ]; then
echo "Changes since ${BASE_TAG}:" >> "$RUNNER_TEMP/release_notes.md"
echo "" >> "$RUNNER_TEMP/release_notes.md"
git log --oneline "${BASE_TAG}..HEAD" --no-merges >> "$RUNNER_TEMP/release_notes.md"
fi

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
with:
Expand Down
26 changes: 25 additions & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ archives:
checksum:
name_template: "checksums.txt"

release:
prerelease: auto

scoops:
- repository:
Expand All @@ -75,6 +77,7 @@ scoops:
homepage: "https://github.com/entireio/cli"
description: "CLI for Entire"
license: MIT
skip_upload: auto

homebrew_casks:
- name: entire
Expand All @@ -91,9 +94,30 @@ homebrew_casks:
bash: "completions/entire.bash"
zsh: "completions/entire.zsh"
fish: "completions/entire.fish"
conflicts:
- cask: entire@nightly
skip_upload: auto

- name: entire@nightly
repository:
owner: entireio
name: homebrew-tap
token: "{{ .Env.TAP_GITHUB_TOKEN }}"
directory: Casks
homepage: "https://github.com/entireio/cli"
description: "CLI for Entire (nightly build)"
binaries:
- entire
completions:
bash: "completions/entire.bash"
zsh: "completions/entire.zsh"
fish: "completions/entire.fish"
conflicts:
- cask: entire
skip_upload: "{{ if not .Prerelease }}true{{ end }}"

announce:
discord:
enabled: '{{ and (isEnvSet "DISCORD_WEBHOOK_ID") (isEnvSet "DISCORD_WEBHOOK_TOKEN") }}'
enabled: '{{ and (not .Prerelease) (isEnvSet "DISCORD_WEBHOOK_ID") (isEnvSet "DISCORD_WEBHOOK_TOKEN") }}'
message_template: "Beep, boop. **Entire CLI {{ .Tag }}** is out!\n\n{{ .ReleaseURL }}\n\nSee https://docs.entire.io/cli/installation for help installing and updating."
author: "Entire"
25 changes: 22 additions & 3 deletions cmd/entire/cli/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"time"

"github.com/entireio/cli/cmd/entire/cli/agent"
"github.com/entireio/cli/cmd/entire/cli/agent/types"
"github.com/entireio/cli/cmd/entire/cli/logging"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/session"
Expand Down Expand Up @@ -79,16 +80,20 @@ func handleLifecycleSessionStart(ctx context.Context, ag agent.Agent, event *age

// Build informational message — warn early if repo has no commits yet,
// since checkpoints require at least one commit to work.
message := "\n\nPowered by Entire:\n This conversation will be linked to your next commit."
message := sessionStartMessage(ag.Name(), false)
if repo, err := strategy.OpenRepository(ctx); err == nil && strategy.IsEmptyRepository(repo) {
message = "\n\nPowered by Entire:\n No commits yet — checkpoints will activate after your first commit."
message = sessionStartMessage(ag.Name(), true)
}

// Check for concurrent sessions and append count if any
_, countSessionsSpan := perf.Start(ctx, "count_active_sessions")
strat := GetStrategy(ctx)
if count, err := strat.CountOtherActiveSessionsWithCheckpoints(ctx, event.SessionID); err == nil && count > 0 {
message += fmt.Sprintf("\n %d other active conversation(s) in this workspace will also be included.\n Use 'entire status' for more information.", count)
if ag.Name() == agent.AgentNameCodex {
message += fmt.Sprintf(" %d other active conversation(s) in this workspace will also be included. Use 'entire status' for more information.", count)
} else {
message += fmt.Sprintf("\n %d other active conversation(s) in this workspace will also be included.\n Use 'entire status' for more information.", count)
}
}
countSessionsSpan.End()

Expand Down Expand Up @@ -135,6 +140,20 @@ func handleLifecycleSessionStart(ctx context.Context, ag agent.Agent, event *age
return nil
}

func sessionStartMessage(agentName types.AgentName, emptyRepo bool) string {
if agentName == agent.AgentNameCodex {
if emptyRepo {
return "Powered by Entire: No commits yet — checkpoints will activate after your first commit."
}
return "Powered by Entire: This conversation will be linked to your next commit."
}

if emptyRepo {
return "\n\nPowered by Entire:\n No commits yet — checkpoints will activate after your first commit."
}
return "\n\nPowered by Entire:\n This conversation will be linked to your next commit."
}

// handleLifecycleModelUpdate persists the model name for the current session.
//
// If the session state file already exists (e.g., Gemini's BeforeModel fires
Expand Down
40 changes: 40 additions & 0 deletions cmd/entire/cli/lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,46 @@ func TestHandleLifecycleSessionStart_DefaultMessageWithCommits(t *testing.T) {
if strings.Contains(ag.lastMessage, "No commits yet") {
t.Errorf("did not expect empty-repo warning, got: %q", ag.lastMessage)
}
if !strings.HasPrefix(ag.lastMessage, "\n\nPowered by Entire:\n ") {
t.Errorf("expected multiline session-start banner, got %q", ag.lastMessage)
}
if strings.Contains(ag.lastMessage, "Powered by Entire: This conversation") {
t.Errorf("expected default agent banner to remain multiline, got %q", ag.lastMessage)
}
}

func TestSessionStartMessage_CodexUsesSingleLineBanner(t *testing.T) {
t.Parallel()

msg := sessionStartMessage(agent.AgentNameCodex, false)
require.Equal(t, "Powered by Entire: This conversation will be linked to your next commit.", msg)
if strings.Contains(msg, "\n") {
t.Fatalf("expected single-line Codex message, got %q", msg)
}
}

func TestSessionStartMessage_CodexUsesSingleLineBannerForEmptyRepo(t *testing.T) {
t.Parallel()

msg := sessionStartMessage(agent.AgentNameCodex, true)
require.Equal(t, "Powered by Entire: No commits yet — checkpoints will activate after your first commit.", msg)
if strings.Contains(msg, "\n") {
t.Fatalf("expected single-line Codex empty-repo message, got %q", msg)
}
}

func TestHandleLifecycleSessionStart_CodexConcurrentSessionsStaySingleLine(t *testing.T) {
t.Parallel()

msg := sessionStartMessage(agent.AgentNameCodex, false)
msg += " 1 other active conversation(s) in this workspace will also be included. Use 'entire status' for more information."

if strings.Contains(msg, "\n") {
t.Fatalf("expected Codex concurrent-session message to stay single-line, got %q", msg)
}
if strings.Contains(msg, " ") {
t.Fatalf("expected Codex concurrent-session message to avoid repeated spaces, got %q", msg)
}
}

// --- handleLifecycleTurnStart tests ---
Expand Down
6 changes: 3 additions & 3 deletions cmd/entire/cli/transcript/compact/codex.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

const (
codexTypeMessage = "message"
transcriptTypeMessage = "message"
codexTypeFunctionCall = "function_call"
codexTypeFunctionCallOutput = "function_call_output"
)
Expand Down Expand Up @@ -98,7 +98,7 @@ func compactCodex(content []byte, opts MetadataFields) ([]byte, error) {
}

switch {
case p.Type == codexTypeMessage && p.Role == "user":
case p.Type == transcriptTypeMessage && p.Role == "user":
text := codexUserText(p.Content)
if text == "" {
continue
Expand All @@ -113,7 +113,7 @@ func compactCodex(content []byte, opts MetadataFields) ([]byte, error) {
line.Content = contentJSON
appendLine(&result, line)

case p.Type == codexTypeMessage && p.Role == "assistant":
case p.Type == transcriptTypeMessage && p.Role == "assistant":
text := codexAssistantText(p.Content)
if text == "" {
continue
Expand Down
4 changes: 4 additions & 0 deletions cmd/entire/cli/transcript/compact/compact.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ func Compact(content []byte, opts MetadataFields) ([]byte, error) {
truncated = []byte{}
}

if isCopilotFormat(truncated) {
return compactCopilot(truncated, opts)
}

if isDroidFormat(truncated) {
return compactDroid(truncated, opts)
}
Expand Down
Loading
Loading