diff --git a/docs/src/content/docs/blog/2026-02-08-authoring-workflows.md b/docs/src/content/docs/blog/2026-02-08-authoring-workflows.md
index 32c3942887..4a98f71efe 100644
--- a/docs/src/content/docs/blog/2026-02-08-authoring-workflows.md
+++ b/docs/src/content/docs/blog/2026-02-08-authoring-workflows.md
@@ -10,9 +10,6 @@ draft: true
prev:
link: /gh-aw/blog/2026-02-05-how-workflows-work/
label: How Workflows Work
-next:
- link: /gh-aw/blog/2026-02-11-getting-started/
- label: Getting Started
---
[Previous Article](/gh-aw/blog/2026-02-05-how-workflows-work/)
diff --git a/docs/src/content/docs/blog/2026-02-11-getting-started.md b/docs/src/content/docs/blog/2026-02-11-getting-started.md
deleted file mode 100644
index 4d6e88a236..0000000000
--- a/docs/src/content/docs/blog/2026-02-11-getting-started.md
+++ /dev/null
@@ -1,470 +0,0 @@
----
-title: "Getting Started with Agentic Workflows"
-description: "Begin your journey with agentic automation"
-authors:
- - dsyme
- - pelikhan
- - mnkiefer
-date: 2026-02-11
-draft: true
-prev:
- link: /gh-aw/blog/2026-02-08-authoring-workflows/
- label: Authoring Workflows
----
-
-[Previous Article](/gh-aw/blog/2026-02-08-authoring-workflows/)
-
----
-
-
-
-We've reached the *grand conclusion* of our Peli's Agent Factory series! You've toured the [workflows](/gh-aw/blog/2026-01-13-meet-the-workflows/), discovered [lessons](/gh-aw/blog/2026-01-21-twelve-lessons/), learned the [patterns](/gh-aw/blog/2026-01-24-design-patterns/), mastered [operations](/gh-aw/blog/2026-01-27-operational-patterns/), explored [imports](/gh-aw/blog/2026-01-30-imports-and-sharing/), secured the [vault](/gh-aw/blog/2026-02-02-security-lessons/), glimpsed the [magnificent machinery](/gh-aw/blog/2026-02-05-how-workflows-work/), and practiced [authoring](/gh-aw/blog/2026-02-08-authoring-workflows/). Now for the *golden ticket* - your practical getting started guide!
-
-Ready to build your own agent ecosystem? Let's get you up and running!
-
-This guide will take you from zero to your first running workflow in just a few minutes, then show you how to grow from there. We'll start simple, build confidence, and then explore what's possible. By the end, you'll have a solid foundation for agentic automation.
-
-Let's do this! π
-
-## Quick Start: Your First Workflow in 5 Minutes
-
-The fastest way to experience agentic workflows is to install a working example. We'll walk you through it step by step.
-
-### Prerequisites
-
-Before starting, make sure you have:
-
-- β
**GitHub CLI** (`gh`) - [Install here](https://cli.github.com) v2.0.0+
-- β
**GitHub account** with admin or write access to a repository
-- β
**GitHub Actions** enabled in your repository
-- β
**Git** installed on your machine
-- β
**Operating System**: Linux, macOS, or Windows with WSL
-
-**Verify your setup:**
-
-```bash
-gh --version # Should show version 2.0.0 or higher
-gh auth status # Should show "Logged in to github.com"
-git --version # Should show git version 2.x or higher
-```
-
-Looking good? Let's keep going!
-
-### Step 1: Install the Extension
-
-Install the GitHub Agentic Workflows CLI extension:
-
-```bash
-gh extension install githubnext/gh-aw
-```
-
-:::note
-If you're working in GitHub Codespaces and the installation fails, use the standalone installer:
-
-```bash
-curl -sL https://raw.githubusercontent.com/githubnext/gh-aw/main/install-gh-aw.sh | bash
-```
-
-:::
-
-Easy, right?
-
-### Step 2: Add a Sample Workflow
-
-Navigate to your repository and install a sample workflow from the [Agentics Collection](https://github.com/githubnext/agentics):
-
-```bash
-gh aw add githubnext/agentics/daily-team-status --create-pull-request
-```
-
-This creates a pull request that adds:
-
-- `.github/workflows/daily-team-status.md` (the natural language workflow)
-- `.github/workflows/daily-team-status.lock.yml` (the compiled GitHub Actions workflow)
-
-Review the PR and merge it into your repository. You're doing great!
-
-### Step 3: Configure AI Authentication
-
-Workflows need to authenticate with an AI service. By default, they use **GitHub Copilot**.
-
-#### Create a Personal Access Token (PAT)
-
-1. Visit
-2. Configure the token:
- - **Token name**: "Agentic Workflows Copilot"
- - **Expiration**: 90 days (recommended for testing)
- - **Resource owner**: Your personal account
- - **Repository access**: "Public repositories" or "All repositories"
-3. Add permissions:
- - In **"Account permissions"** (not Repository permissions)
- - Find **"Copilot Requests"**
- - Set to **"Access: Read"**
-4. Click **"Generate token"** and copy it immediately
-
-:::tip
-Can't find "Copilot Requests" permission? Make sure you have:
-
-- An active [GitHub Copilot subscription](https://github.com/settings/copilot)
-- A fine-grained token (not classic)
-- Personal account as Resource owner
-- Public or all repositories selected
-
-:::
-
-#### Add Token to Your Repository
-
-1. Go to your repository β **Settings** β **Secrets and variables** β **Actions**
-2. Click **"New repository secret"**
-3. Set **Name** to `COPILOT_GITHUB_TOKEN`
-4. Paste the token in **Secret**
-5. Click **"Add secret"**
-
-Perfect! You're almost there.
-
-### Step 4: Verify Setup
-
-Check that everything is configured correctly:
-
-```bash
-gh aw status
-```
-
-**Expected output:**
-
-```text
-Workflow Engine State Enabled Schedule
-ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-daily-team-status copilot β Yes 0 9 * * 1-5
-```
-
-Looking good!
-
-### Step 5: Run Your First Workflow
-
-Trigger the workflow immediately (no need to wait for the schedule):
-
-```bash
-gh aw run daily-team-status
-```
-
-After a minute or two, check the results:
-
-```bash
-gh aw status
-```
-
-Once complete, check your repository's **Discussions** section for the generated team status report!
-
-π **Congratulations!** You've just run your first agentic workflow!
-
-## Growth Path: From One to Many
-
-Now that you have one workflow running, here's how to grow your agent ecosystem:
-
-### Phase 1: Learn by Example (Week 1)
-
-**Run multiple example workflows to understand patterns:**
-
-```bash
-# Add a triage agent
-gh aw add githubnext/agentics/issue-triage
-
-# Add a CI doctor
-gh aw add githubnext/agentics/ci-doctor
-
-# Add a weekly summary
-gh aw add githubnext/agentics/weekly-research
-```
-
-Observe how different workflows:
-
-- Trigger on different events
-- Use different tools
-- Create different outputs
-- Serve different purposes
-
-### Phase 2: Customize Examples (Week 2)
-
-**Modify existing workflows to fit your needs:**
-
-1. Copy a workflow you like:
-
- ```bash
- cp .github/workflows/issue-triage.md .github/workflows/my-triage.md
- ```
-
-2. Edit the prompt to match your repository:
- - Change label names
- - Adjust categories
- - Add custom rules
- - Update terminology
-
-3. Recompile:
-
- ```bash
- gh aw compile .github/workflows/my-triage.md
- ```
-
-4. Test manually:
-
- ```bash
- gh aw run my-triage
- ```
-
-### Phase 3: Create Original Workflows (Week 3+)
-
-**Build workflows specific to your team's needs:**
-
-Start with a simple Read-Only Analyst:
-
-```markdown
----
-description: Weekly dependency report
-on:
- schedule: "0 9 * * 1" # Monday mornings
-permissions:
- contents: read
-safe_outputs:
- create_discussion:
- title: "Dependency Report - {date}"
- category: "Reports"
-imports:
- - shared/reporting.md
----
-
-## Weekly Dependency Analysis
-
-Analyze package.json (or requirements.txt, go.mod, etc.):
-
-1. List all dependencies
-2. Check for available updates
-3. Identify security vulnerabilities
-4. Prioritize updates by importance
-
-Create a discussion with:
-- Summary of dependency health
-- List of available updates
-- Security alerts
-- Recommended actions
-```
-
-### Phase 4: Build Your Factory (Ongoing)
-
-**Systematically address pain points:**
-
-For each repetitive task, ask:
-
-1. Could an agent do this?
-2. What pattern fits best?
-3. What's the minimum viable version?
-4. How can we test it safely?
-
-**Common starting points:**
-
-- Issue triage and labeling
-- CI failure diagnosis
-- Documentation updates
-- Weekly metrics reports
-- Security scanning
-- Code quality checks
-
-## Essential Commands Reference
-
-### Workflow Management
-
-```bash
-# List all workflows
-gh aw list
-
-# Show workflow status
-gh aw status [workflow-name]
-
-# Add workflow from collection
-gh aw add [--create-pull-request]
-
-# Compile workflow
-gh aw compile
-
-# Run workflow manually
-gh aw run
-
-# Download workflow logs
-gh aw logs
-```
-
-### Secret Management
-
-```bash
-# Configure AI engine secrets
-gh aw secrets bootstrap --engine copilot
-
-# List required secrets
-gh aw secrets list
-
-# Validate secret configuration
-gh aw secrets validate
-```
-
-### Debugging
-
-```bash
-# Validate workflow syntax
-gh aw validate
-
-# Show compilation output
-gh aw compile --output preview.yml
-
-# Audit workflow runs
-gh aw audit
-
-# Inspect MCP configuration
-gh aw mcp inspect
-```
-
-## Best Practices for Beginners
-
-### Start Small
-
-β
**Do**: Begin with read-only analyst workflows
-β **Don't**: Start with workflows that modify code
-
-### Test Manually First
-
-β
**Do**: Use `workflow_dispatch` triggers initially
-β **Don't**: Deploy directly to automatic schedules
-
-### Use Time Limits
-
-β
**Do**: Add `stop-after: "+1mo"` to experiments
-β **Don't**: Let experimental workflows run indefinitely
-
-### Copy Successful Patterns
-
-β
**Do**: Clone and modify working workflows
-β **Don't**: Build everything from scratch
-
-### Review Every Output
-
-β
**Do**: Check issues, PRs, and discussions agents create
-β **Don't**: Assume agents always get it right
-
-### Iterate Gradually
-
-β
**Do**: Make small changes, test, adjust
-β **Don't**: Make large changes without testing
-
-## Common First-Week Questions
-
-### "Which AI engine should I use?"
-
-**Start with Copilot** (default). It's integrated with GitHub and uses your Copilot subscription. Try other engines later:
-
-- **Claude**: For longer context and detailed analysis
-- **Codex**: For enterprise Azure integration
-- **Custom**: For proprietary or specialized models
-
-### "How do I handle secrets?"
-
-Use repository secrets (Settings β Secrets β Actions):
-
-- `COPILOT_GITHUB_TOKEN` for Copilot
-- `ANTHROPIC_API_KEY` for Claude
-- `AZURE_OPENAI_*` for Codex
-
-Never put secrets in workflow files!
-
-### "What if a workflow creates too many issues?"
-
-Use safe output guardrails:
-
-```yaml
-safe_outputs:
- create_issue:
- max_items: 3 # Limit to 3
- close_older: true # Close duplicates
- expire: "+7d" # Auto-close after 7 days
-```
-
-### "How much does this cost?"
-
-Costs depend on:
-
-- **GitHub Actions**: Free tier covers many workflows
-- **AI API calls**: Billed per request/token
-- **Copilot**: Included in Copilot subscription
-
-Start with free tier, monitor usage with `gh aw audit`.
-
-### "Can I use this in production?"
-
-β οΈ **GitHub Agentic Workflows is a research demonstrator** in early development. Use with caution:
-
-- Review all agent outputs
-- Use time-limited trials
-- Implement human approval gates
-- Monitor security alerts
-- Have rollback plans
-
-### "Where can I get help?"
-
-Resources:
-
-- **Documentation**:
-- **Examples**:
-- **Discussions**:
-- **Discord**: [GitHub Next Discord](https://gh.io/next-discord) #continuous-ai
-
-## Your First Week Plan
-
-### Day 1: Installation and Setup
-
-- Install gh-aw extension
-- Add first sample workflow
-- Configure authentication
-- Run first workflow successfully
-
-### Day 2-3: Exploration
-
-- Install 3-5 different workflow types
-- Observe how they behave
-- Review their outputs
-- Identify patterns
-
-### Day 4-5: Customization
-
-- Pick your favorite workflow
-- Modify it for your repository
-- Test the changes
-- Deploy to schedule
-
-### Day 6-7: Creation
-
-- Identify a pain point in your workflow
-- Find similar example workflow
-- Adapt it to your needs
-- Start with manual trigger only
-
-## Next Steps
-
-Once you're comfortable with the basics:
-
-1. **Study the patterns** - Review [12 Design Patterns](03-design-patterns.md)
-2. **Explore advanced features** - Repo-memory, multi-phase workflows
-3. **Join the community** - Share your workflows
-4. **Contribute back** - Add your workflows to Agentics collection
-5. **Build your factory** - Create an ecosystem of cooperating agents
-
-## Welcome to the Factory
-
-You're now part of a growing community exploring the frontier of automated agentic development. Start small, experiment safely, and share what you learn.
-
-The agents you build today will help shape the future of software development.
-
-**Ready to build your first workflow?** Head over to the [documentation](https://githubnext.github.io/gh-aw/) and start experimenting!
-
-## What's Next?
-
-_More articles in this series coming soon._
-
-[Previous Article](/gh-aw/blog/2026-02-08-authoring-workflows/)
diff --git a/docs/src/content/docs/setup/quick-start.md b/docs/src/content/docs/setup/quick-start.md
index 22425209b5..55bd580d16 100644
--- a/docs/src/content/docs/setup/quick-start.md
+++ b/docs/src/content/docs/setup/quick-start.md
@@ -5,32 +5,23 @@ sidebar:
order: 1
---
-## Adding a Daily Status Workflow to Your Repo
+## Adding an Automated Daily Status Workflow to Your Repo
-In this guide you will add the automated [**Daily Status Report**](https://github.com/githubnext/agentics/blob/main/workflows/daily-team-status.md?plain=1) to an existing GitHub repository where you are a maintainer.
+In this guide you will add the automated [**Daily Repo Status Report**](https://github.com/githubnext/agentics/blob/main/workflows/daily-repo-status.md?plain=1) to an existing GitHub repository where you are a maintainer, running in GitHub Actions.
-Remember the aim here is _automated AI_: to install something that will run _automatically_ every day, in the context of your repository, and create a fresh status report issue in your repository without any further manual intervention.
+Remember the aim here is _automated AI_: to install something that will run _automatically_ every day, in the context of your repository, and create a fresh status report issue in your repository without any further manual intervention. If you're familiar with GitHub Actions, you will be aware of the power of automation.
-There are hundreds of other ways to use GitHub Agentic Workflows too, which you can explore in [Peli's Agent Factory](https://githubnext.github.io/gh-aw/blog/2026-01-12-welcome-to-pelis-agent-factory/). This workflow is just the start of what's possible, to get you familiar with the installation and setup process.
+There are hundreds of other ways to use GitHub Agentic Workflows, which you can explore in [Peli's Agent Factory](https://githubnext.github.io/gh-aw/blog/2026-01-12-welcome-to-pelis-agent-factory/). This workflow is just the start of what's possible.
## Prerequisites
Before installing, ensure you have:
-- β
**AI Account:** A GitHub Copilot, Anthropic Claude or OpenAI Codex subscription
-- β
**GitHub Repository** you are a maintainer on
-- β
**[GitHub Actions](https://docs.github.com/actions)** enabled in your repository
-- β
**GitHub CLI** (`gh`) - A command-line tool for GitHub operations. [Install here](https://cli.github.com) v2.0.0+ and authenticate with `gh auth login`
-- β
**Git** installed on your machine
-- β
**Operating System:** Linux, macOS, or Windows with WSL
-
-**Verify your setup:**
-
-```bash
-gh --version # Should show version 2.0.0 or higher
-gh auth status # Should show "Logged in to github.com"
-git --version # Should show git version 2.x or higher
-```
+- β
**AI Account** - a GitHub Copilot, Anthropic Claude or OpenAI Codex subscription
+- β
**GitHub Repository** - a GitHub repository you are a maintainer on
+- β
**GitHub Actions** enabled in your repository
+- β
**GitHub CLI** (`gh`) - A command-line tool for GitHub. [Install here](https://cli.github.com) v2.0.0+ and authenticate with `gh auth login`
+- β
**Operating System**: Linux, macOS, or Windows with WSL
### Step 1 β Install the extension
@@ -41,123 +32,52 @@ gh extension install githubnext/gh-aw
```
> [!TIP]
-> Working in GitHub Codespaces?
->
> If you're working in a GitHub Codespace, use the standalone installer instead:
>
> ```bash wrap
> curl -sL https://raw.githubusercontent.com/githubnext/gh-aw/main/install-gh-aw.sh | bash
> ```
-### Step 2 β Initialize Agentic Workflows support in your repository
+### Step 2 β Add the sample workflow
-Initialize agentic workflows in your repository, to configure optional additional supporting files and settings:
+From your repository root run:
```bash wrap
-gh aw init --push
+gh aw add githubnext/agentics/daily-repo-status
```
-This command installs tools and automatically commits and pushes the changes to your repository.
-
-> [!TIP]
->
-> If you have branch protection rules enabled, replace `--push` with `--create-pull-request`, then review and merge the pull request.
-
-### Step 3 β Add a sample workflow
+This will take you through an interactive process to
-Add a sample from the [agentics](https://github.com/githubnext/agentics) collection. From your repository root run:
+1. Select an AI Engine to use
+2. Add the workflow and set up required secrets
+3. Trigger an initial run of the workflow
-```bash wrap
-gh aw add githubnext/agentics/daily-team-status --push
-```
+### Step 3 β Looking at the results
-This adds `.github/workflows/daily-team-status.md` and `.github/workflows/daily-team-status.lock.yml` to your repository. The second file is the [compiled](/gh-aw/reference/glossary/#compilation) GitHub Actions workflow file corresponding to the agentic workflow.
+All going well, you have now successfully installed your first automated agentic workflow into your repository and triggered an initial run.
-> [!TIP]
->
-> If you have branch protection rules enabled, replace `--push` with `--create-pull-request`, then review and merge the pull request.
-
-### Step 4 β Add an AI secret (Copilot Users)
-
-[Agentic workflows](/gh-aw/reference/glossary/#agentic-workflow) need to authenticate with an AI service to execute. By default, they use **GitHub Copilot** as the AI service, but you can also use **Anthropic Claude** or **OpenAI Codex**.
-
-The instructions below assume you have an active [GitHub Copilot subscription](https://github.com/settings/copilot). Claude/Codex Users see [AI Engines](/gh-aw/reference/engines/).
-
-#### Copilot Users: Create a Personal Access Token (PAT)
-
-Create a [Personal Access Token](/gh-aw/reference/glossary/#personal-access-token-pat) to authenticate your workflows with GitHub Copilot:
-
-1. Visit
-2. Configure the token:
- - **Token name**: "Agentic Workflows Copilot"
- - **Expiration**: 90 days (recommended for testing)
- - **Resource owner**: Your personal account (required for Copilot Requests permission)
- - **Repository access**: "Public repositories" (required for Copilot Requests permission to appear)
-3. Add permissions:
- - In **"Account permissions"** (not Repository permissions), find **"Copilot Requests"**
- - Set to **"Access: Read"**
-4. Click **"Generate token"** and copy it immediately (you won't see it again)
-
-> [!TIP]
-> Can't find Copilot Requests permission?
->
-> This requires an active [GitHub Copilot subscription](https://github.com/settings/copilot), a fine-grained token (not classic), personal account as Resource owner, and "Public repositories" or "All repositories" selected. Contact your GitHub administrator if Copilot is managed by your organization.
->
+Once complete, a new issue will be created in your repository with a "repo status report". The report will be automatically generated by the AI based on recent activity in your repository, including issues, PRs, discussions, releases, and code changes.
-#### Add the token to your repository
+### Step 4 β Customize your workflow
-Store the token as a repository secret:
+You can now customize the workflow by editing the workflow markdown file located at `.github/workflows/daily-repo-status.md` in your repository. You then run
-1. Go to **your repository** β **Settings** β **Secrets and variables** β **Actions**
-2. Click **New repository secret**
-3. Set **Name** to `COPILOT_GITHUB_TOKEN` and paste the token in **Secret**
-4. Click **Add secret**
-
-Repository secrets are encrypted and only accessible to workflows in your repository. See [GitHub Copilot CLI documentation](https://github.com/github/copilot-cli?tab=readme-ov-file#authenticate-with-a-personal-access-token-pat) for more details.
-
-#### Verify your setup
-
-Before running workflows, verify everything is configured correctly:
-
-```bash wrap
-gh aw status
-```
-
-**Expected output:**
-
-```text
-βββββββββββββββββββ¬ββββββββ¬βββββββββ¬βββββββ¬βββββββββββββββ¬βββββββ¬βββββββββββ¬βββββββββββββββ
-βWorkflow βEngine βCompiledβStatusβTime RemainingβLabelsβRun StatusβRun Conclusionβ
-βββββββββββββββββββΌββββββββΌβββββββββΌβββββββΌβββββββββββββββΌβββββββΌβββββββββββΌβββββββββββββββ€
-βdaily-team-statusβcopilotβNo βactiveβ30d 22h β- β- β- β
-βββββββββββββββββββ΄ββββββββ΄βββββββββ΄βββββββ΄βββββββββββββββ΄βββββββ΄βββββββββββ΄βββββββββββββββ
-```
-
-This confirms the workflow is compiled, enabled, and scheduled correctly.
-
-> [!TIP]
-> Troubleshooting
->
-> If the workflow isn't listed, run `gh aw compile` and verify `.github/workflows/daily-team-status.md` exists, and add and push it to your repo. If errors occur when running, verify the `COPILOT_GITHUB_TOKEN` secret is set with "Copilot Requests" permission and hasn't expired. Run `gh aw secrets bootstrap --engine copilot` to check configuration.
-
-### Step 5 β Trigger a workflow run
-
-Trigger the workflow immediately in GitHub Actions (this may fail in a codespace):
-
-```bash wrap
-gh aw run daily-team-status
+```bash
+gh aw compile
```
-After a few moments, check the status:
+to regenerate the workflow YAML file, and push to your repository. You can then trigger another run by running:
-```bash wrap
-gh aw status
+```bash
+gh aw run daily-repo-status
```
-Once complete, a new issue will be created in your repository with a daily team status report! The report will be automatically generated by the AI based on recent activity in your repository, including issues, PRs, discussions, releases, and code changes.
+## What's next?
-You have successfully installed your first automated agentic workflow into your repository.
+With that, you are up and running with your first automated agentic workflow!
-## What's next?
+Explore further with:
-Next up is [Authoring Agentic Workflows](/gh-aw/setup/agentic-authoring/) where you will learn how to create automated workflows with AI assistance. You can also explore the samples in [Peli's Agent Factory](/gh-aw/blog/2026-01-12-welcome-to-pelis-agent-factory/). To understand how agentic workflows work, read [How They Work](/gh-aw/introduction/how-they-work/).
+- [Authoring Agentic Workflows using AI](/gh-aw/setup/agentic-authoring/) where you will learn how to create automated workflows with AI assistance.
+- Explore the samples in [Peli's Agent Factory](/gh-aw/blog/2026-01-12-welcome-to-pelis-agent-factory/).
+- Understand [How Agentic Workflows Work](/gh-aw/introduction/how-they-work/).
diff --git a/install.md b/install.md
index 0d4ed88695..7a1478e2bc 100644
--- a/install.md
+++ b/install.md
@@ -140,7 +140,7 @@ gh aw add githubnext/agentics
This shows available workflows. Add one:
```bash
-gh aw add githubnext/agentics/daily-team-status --create-pull-request
+gh aw add githubnext/agentics/daily-repo-status
```
**Option B: Use the AI agent to create workflows**
diff --git a/pkg/cli/add_command.go b/pkg/cli/add_command.go
index 2f6e2360ed..e7ea66e999 100644
--- a/pkg/cli/add_command.go
+++ b/pkg/cli/add_command.go
@@ -1,6 +1,7 @@
package cli
import (
+ "context"
"fmt"
"math/rand"
"os"
@@ -12,12 +13,50 @@ import (
"github.com/githubnext/gh-aw/pkg/console"
"github.com/githubnext/gh-aw/pkg/constants"
"github.com/githubnext/gh-aw/pkg/logger"
+ "github.com/githubnext/gh-aw/pkg/parser"
+ "github.com/githubnext/gh-aw/pkg/tty"
"github.com/githubnext/gh-aw/pkg/workflow"
"github.com/spf13/cobra"
)
var addLog = logger.New("cli:add_command")
+// AddWorkflowsResult contains the result of adding workflows
+type AddWorkflowsResult struct {
+ // PRNumber is the PR number if a PR was created, or 0 if no PR was created
+ PRNumber int
+ // PRURL is the URL of the created PR, or empty if no PR was created
+ PRURL string
+ // HasWorkflowDispatch is true if any of the added workflows has a workflow_dispatch trigger
+ HasWorkflowDispatch bool
+}
+
+// ResolvedWorkflow contains metadata about a workflow that has been resolved and is ready to add
+type ResolvedWorkflow struct {
+ // Spec is the parsed workflow specification
+ Spec *WorkflowSpec
+ // Content is the raw workflow content
+ Content []byte
+ // SourceInfo contains source metadata (package path, commit SHA)
+ SourceInfo *WorkflowSourceInfo
+ // Description is the workflow description extracted from frontmatter
+ Description string
+ // Engine is the preferred engine extracted from frontmatter (empty if not specified)
+ Engine string
+ // HasWorkflowDispatch indicates if the workflow has workflow_dispatch trigger
+ HasWorkflowDispatch bool
+}
+
+// ResolvedWorkflows contains all resolved workflows ready to be added
+type ResolvedWorkflows struct {
+ // Workflows is the list of resolved workflows
+ Workflows []*ResolvedWorkflow
+ // HasWildcard indicates if any of the original specs contained wildcards
+ HasWildcard bool
+ // HasWorkflowDispatch is true if any of the workflows has a workflow_dispatch trigger
+ HasWorkflowDispatch bool
+}
+
// NewAddCommand creates the add command
func NewAddCommand(validateEngine func(string) error) *cobra.Command {
cmd := &cobra.Command{
@@ -25,9 +64,18 @@ func NewAddCommand(validateEngine func(string) error) *cobra.Command {
Short: "Add agentic workflows from repositories to .github/workflows",
Long: `Add one or more workflows from repositories to .github/workflows.
+By default, this command runs in interactive mode, which guides you through:
+ - Selecting an AI engine (Copilot, Claude, or Codex)
+ - Configuring API keys and secrets
+ - Creating a pull request with the workflow
+ - Optionally running the workflow
+
+Use --non-interactive to skip the guided setup and add workflows directly.
+
Examples:
+ ` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/daily-repo-status # Interactive setup (recommended)
` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics # List available workflows
- ` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/ci-doctor # Add specific workflow
+ ` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/ci-doctor --non-interactive # Skip interactive mode
` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/ci-doctor@v1.0.0 # Add with version
` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/workflows/ci-doctor.md@main
` + string(constants.CLIExtensionPrefix) + ` add https://github.com/githubnext/agentics/blob/main/workflows/ci-doctor.md
@@ -50,6 +98,7 @@ The --dir flag allows you to specify a subdirectory under .github/workflows/ whe
The --create-pull-request flag (or --pr) automatically creates a pull request with the workflow changes.
The --push flag automatically commits and pushes changes after successful workflow addition.
The --force flag overwrites existing workflow files.
+The --non-interactive flag skips the guided setup and uses traditional behavior.
Note: To create a new workflow from scratch, use the 'new' command instead.`,
Args: cobra.MinimumNArgs(1),
@@ -69,17 +118,39 @@ Note: To create a new workflow from scratch, use the 'new' command instead.`,
workflowDir, _ := cmd.Flags().GetString("dir")
noStopAfter, _ := cmd.Flags().GetBool("no-stop-after")
stopAfter, _ := cmd.Flags().GetString("stop-after")
+ nonInteractive, _ := cmd.Flags().GetBool("non-interactive")
if err := validateEngine(engineOverride); err != nil {
return err
}
- // Handle normal mode
- if prFlag {
- return AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, true, pushFlag, noGitattributes, workflowDir, noStopAfter, stopAfter)
- } else {
- return AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, false, pushFlag, noGitattributes, workflowDir, noStopAfter, stopAfter)
+ // Determine if we should use interactive mode
+ // Interactive mode is the default for TTY unless:
+ // - --non-interactive flag is set
+ // - Any of the batch/automation flags are set (--create-pull-request, --force, --name, --number > 1, --append)
+ // - Not a TTY (piped input/output)
+ // - In CI environment
+ // - This is a repo-only spec (listing workflows)
+ useInteractive := !nonInteractive &&
+ !prFlag &&
+ !forceFlag &&
+ nameFlag == "" &&
+ numberFlag == 1 &&
+ appendText == "" &&
+ tty.IsStdoutTerminal() &&
+ os.Getenv("CI") == "" &&
+ os.Getenv("GO_TEST_MODE") != "true" &&
+ !isRepoOnlySpec(workflows[0])
+
+ if useInteractive {
+ addLog.Print("Using interactive mode")
+ ctx := context.Background()
+ return RunAddInteractive(ctx, workflows, verbose, engineOverride, noGitattributes, workflowDir, noStopAfter, stopAfter)
}
+
+ // Handle normal (non-interactive) mode
+ _, err := AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, prFlag, pushFlag, noGitattributes, workflowDir, noStopAfter, stopAfter)
+ return err
},
}
@@ -121,6 +192,9 @@ Note: To create a new workflow from scratch, use the 'new' command instead.`,
// Add stop-after flag to add command
cmd.Flags().String("stop-after", "", "Override stop-after value in the workflow (e.g., '+48h', '2025-12-31 23:59:59')")
+ // Add non-interactive flag to add command
+ cmd.Flags().Bool("non-interactive", false, "Skip interactive setup and use traditional behavior (for CI/automation)")
+
// Register completions for add command
RegisterEngineFlagCompletion(cmd)
RegisterDirFlagCompletion(cmd, "dir")
@@ -128,69 +202,40 @@ Note: To create a new workflow from scratch, use the 'new' command instead.`,
return cmd
}
-// AddWorkflows adds one or more workflows from components to .github/workflows
-// with optional repository installation and PR creation
-func AddWorkflows(workflows []string, number int, verbose bool, engineOverride string, name string, force bool, appendText string, createPR bool, push bool, noGitattributes bool, workflowDir string, noStopAfter bool, stopAfter string) error {
- addLog.Printf("Adding workflows: count=%d, engineOverride=%s, createPR=%v, push=%v, noGitattributes=%v, workflowDir=%s, noStopAfter=%v, stopAfter=%s", len(workflows), engineOverride, createPR, push, noGitattributes, workflowDir, noStopAfter, stopAfter)
+// ResolveWorkflows resolves workflow specifications by parsing specs, installing repositories,
+// expanding wildcards, and fetching workflow content (including descriptions).
+// This is useful for showing workflow information before actually adding them.
+func ResolveWorkflows(workflows []string, verbose bool) (*ResolvedWorkflows, error) {
+ addLog.Printf("Resolving workflows: count=%d", len(workflows))
if len(workflows) == 0 {
- return fmt.Errorf("at least one workflow name is required")
+ return nil, fmt.Errorf("at least one workflow name is required")
}
for i, workflow := range workflows {
if workflow == "" {
- return fmt.Errorf("workflow name cannot be empty (workflow %d)", i+1)
- }
- }
-
- // Check if this is a repo-only specification (owner/repo instead of owner/repo/workflow)
- // If so, list available workflows and exit
- if len(workflows) == 1 && isRepoOnlySpec(workflows[0]) {
- return handleRepoOnlySpec(workflows[0], verbose)
- }
-
- // If creating a PR or pushing, check prerequisites
- if createPR || push {
- // Check if we're in a git repository
- if !isGitRepo() {
- if createPR {
- return fmt.Errorf("not in a git repository - PR creation requires a git repository")
- }
- return fmt.Errorf("not in a git repository - push requires a git repository")
- }
-
- // Check no other changes are present
- if err := checkCleanWorkingDirectory(verbose); err != nil {
- if createPR {
- return fmt.Errorf("working directory is not clean: %w", err)
- }
- return fmt.Errorf("--push requires a clean working directory: %w", err)
- }
-
- // Check if GitHub CLI is available (only for PR)
- if createPR && !isGHCLIAvailable() {
- return fmt.Errorf("GitHub CLI (gh) is required for PR creation but not available")
+ return nil, fmt.Errorf("workflow name cannot be empty (workflow %d)", i+1)
}
}
// Parse workflow specifications and group by repository
repoVersions := make(map[string]string) // repo -> version
- processedWorkflows := []*WorkflowSpec{} // List of processed workflow specs
+ parsedSpecs := []*WorkflowSpec{} // List of parsed workflow specs
for _, workflow := range workflows {
spec, err := parseWorkflowSpec(workflow)
if err != nil {
- return fmt.Errorf("invalid workflow specification '%s': %w", workflow, err)
+ return nil, fmt.Errorf("invalid workflow specification '%s': %w", workflow, err)
}
// Handle repository installation and workflow name extraction
if existing, exists := repoVersions[spec.RepoSlug]; exists && existing != spec.Version {
- return fmt.Errorf("conflicting versions for repository %s: %s vs %s", spec.RepoSlug, existing, spec.Version)
+ return nil, fmt.Errorf("conflicting versions for repository %s: %s vs %s", spec.RepoSlug, existing, spec.Version)
}
repoVersions[spec.RepoSlug] = spec.Version
// Create qualified name for processing
- processedWorkflows = append(processedWorkflows, spec)
+ parsedSpecs = append(parsedSpecs, spec)
}
// Check if any workflow is from the current repository
@@ -198,14 +243,14 @@ func AddWorkflows(workflows []string, number int, verbose bool, engineOverride s
currentRepoSlug, repoErr := GetCurrentRepoSlug()
if repoErr == nil {
// We successfully determined the current repository, check all workflow specs
- for _, spec := range processedWorkflows {
+ for _, spec := range parsedSpecs {
// Skip local workflow specs (starting with "./")
if strings.HasPrefix(spec.WorkflowPath, "./") {
continue
}
if spec.RepoSlug == currentRepoSlug {
- return fmt.Errorf("cannot add workflows from the current repository (%s). The 'add' command is for installing workflows from other repositories", currentRepoSlug)
+ return nil, fmt.Errorf("cannot add workflows from the current repository (%s). The 'add' command is for installing workflows from other repositories", currentRepoSlug)
}
}
}
@@ -227,13 +272,13 @@ func AddWorkflows(workflows []string, number int, verbose bool, engineOverride s
// Install as global package (not local) to match the behavior expected
if err := InstallPackage(repoWithVersion, verbose); err != nil {
addLog.Printf("Failed to install repository %s: %v", repoWithVersion, err)
- return fmt.Errorf("failed to install repository %s: %w", repoWithVersion, err)
+ return nil, fmt.Errorf("failed to install repository %s: %w", repoWithVersion, err)
}
}
// Check if any workflow specs contain wildcards before expansion
hasWildcard := false
- for _, spec := range processedWorkflows {
+ for _, spec := range parsedSpecs {
if spec.IsWildcard {
hasWildcard = true
break
@@ -242,20 +287,120 @@ func AddWorkflows(workflows []string, number int, verbose bool, engineOverride s
// Expand wildcards after installation
var err error
- processedWorkflows, err = expandWildcardWorkflows(processedWorkflows, verbose)
+ parsedSpecs, err = expandWildcardWorkflows(parsedSpecs, verbose)
if err != nil {
- return err
+ return nil, err
}
+ // Fetch workflow content and metadata for each workflow
+ resolvedWorkflows := make([]*ResolvedWorkflow, 0, len(parsedSpecs))
+ hasWorkflowDispatch := false
+
+ for _, spec := range parsedSpecs {
+ // Fetch workflow content
+ content, sourceInfo, err := findWorkflowInPackageForRepo(spec, verbose)
+ if err != nil {
+ return nil, fmt.Errorf("workflow '%s' not found: %w", spec.WorkflowPath, err)
+ }
+
+ // Extract description from content
+ description := ExtractWorkflowDescription(string(content))
+
+ // Extract engine from content (if specified in frontmatter)
+ engine := ExtractWorkflowEngine(string(content))
+
+ // Check for workflow_dispatch trigger
+ workflowHasDispatch := checkWorkflowHasDispatch(spec, verbose)
+ if workflowHasDispatch {
+ hasWorkflowDispatch = true
+ }
+
+ resolvedWorkflows = append(resolvedWorkflows, &ResolvedWorkflow{
+ Spec: spec,
+ Content: content,
+ SourceInfo: sourceInfo,
+ Description: description,
+ Engine: engine,
+ HasWorkflowDispatch: workflowHasDispatch,
+ })
+ }
+
+ return &ResolvedWorkflows{
+ Workflows: resolvedWorkflows,
+ HasWildcard: hasWildcard,
+ HasWorkflowDispatch: hasWorkflowDispatch,
+ }, nil
+}
+
+// AddWorkflows adds one or more workflows from components to .github/workflows
+// with optional repository installation and PR creation.
+// Returns AddWorkflowsResult containing PR number (if created) and other metadata.
+func AddWorkflows(workflows []string, number int, verbose bool, engineOverride string, name string, force bool, appendText string, createPR bool, push bool, noGitattributes bool, workflowDir string, noStopAfter bool, stopAfter string) (*AddWorkflowsResult, error) {
+ // Check if this is a repo-only specification (owner/repo instead of owner/repo/workflow)
+ // If so, list available workflows and exit
+ if len(workflows) == 1 && isRepoOnlySpec(workflows[0]) {
+ return &AddWorkflowsResult{}, handleRepoOnlySpec(workflows[0], verbose)
+ }
+
+ // Resolve workflows first
+ resolved, err := ResolveWorkflows(workflows, verbose)
+ if err != nil {
+ return nil, err
+ }
+
+ return AddResolvedWorkflows(workflows, resolved, number, verbose, false, engineOverride, name, force, appendText, createPR, push, noGitattributes, workflowDir, noStopAfter, stopAfter)
+}
+
+// AddResolvedWorkflows adds workflows using pre-resolved workflow data.
+// This allows callers to resolve workflows early (e.g., to show descriptions) and then add them later.
+// The quiet parameter suppresses detailed output (useful for interactive mode where output is already shown).
+func AddResolvedWorkflows(workflowStrings []string, resolved *ResolvedWorkflows, number int, verbose bool, quiet bool, engineOverride string, name string, force bool, appendText string, createPR bool, push bool, noGitattributes bool, workflowDir string, noStopAfter bool, stopAfter string) (*AddWorkflowsResult, error) {
+ addLog.Printf("Adding workflows: count=%d, engineOverride=%s, createPR=%v, noGitattributes=%v, workflowDir=%s, noStopAfter=%v, stopAfter=%s", len(workflowStrings), engineOverride, createPR, noGitattributes, workflowDir, noStopAfter, stopAfter)
+
+ result := &AddWorkflowsResult{}
+
+ // If creating a PR, check prerequisites
+ if createPR {
+ // Check if GitHub CLI is available
+ if !isGHCLIAvailable() {
+ return nil, fmt.Errorf("GitHub CLI (gh) is required for PR creation but not available")
+ }
+
+ // Check if we're in a git repository
+ if !isGitRepo() {
+ return nil, fmt.Errorf("not in a git repository - PR creation requires a git repository")
+ }
+
+ // Check no other changes are present
+ if err := checkCleanWorkingDirectory(verbose); err != nil {
+ return nil, fmt.Errorf("working directory is not clean: %w", err)
+ }
+ }
+
+ // Extract the workflow specs for processing
+ processedWorkflows := make([]*WorkflowSpec, len(resolved.Workflows))
+ for i, rw := range resolved.Workflows {
+ processedWorkflows[i] = rw.Spec
+ }
+
+ // Set workflow_dispatch result
+ result.HasWorkflowDispatch = resolved.HasWorkflowDispatch
+
// Handle PR creation workflow
if createPR {
addLog.Print("Creating workflow with PR")
- return addWorkflowsWithPR(processedWorkflows, number, verbose, engineOverride, name, force, appendText, noGitattributes, hasWildcard, workflowDir, noStopAfter, stopAfter)
+ prNumber, prURL, err := addWorkflowsWithPR(processedWorkflows, number, verbose, quiet, engineOverride, name, force, appendText, push, noGitattributes, resolved.HasWildcard, workflowDir, noStopAfter, stopAfter)
+ if err != nil {
+ return nil, err
+ }
+ result.PRNumber = prNumber
+ result.PRURL = prURL
+ return result, nil
}
// Handle normal workflow addition
addLog.Print("Adding workflows normally without PR")
- return addWorkflowsNormal(processedWorkflows, number, verbose, engineOverride, name, force, appendText, push, noGitattributes, hasWildcard, workflowDir, noStopAfter, stopAfter)
+ return result, addWorkflowsNormal(processedWorkflows, number, verbose, quiet, engineOverride, name, force, appendText, push, noGitattributes, resolved.HasWildcard, workflowDir, noStopAfter, stopAfter)
}
// handleRepoOnlySpec handles the case when user provides only owner/repo without workflow name
@@ -410,7 +555,7 @@ func displayAvailableWorkflows(repoSlug, version string, verbose bool) error {
}
// addWorkflowsNormal handles normal workflow addition without PR creation
-func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, push bool, noGitattributes bool, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string) error {
+func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, quiet bool, engineOverride string, name string, force bool, appendText string, push bool, noGitattributes bool, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string) error {
// Create file tracker for all operations
tracker, err := NewFileTracker()
if err != nil {
@@ -435,13 +580,13 @@ func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, eng
}
}
- if len(workflows) > 1 {
+ if !quiet && len(workflows) > 1 {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Adding %d workflow(s)...", len(workflows))))
}
// Add each workflow
for i, workflow := range workflows {
- if len(workflows) > 1 {
+ if !quiet && len(workflows) > 1 {
fmt.Fprintln(os.Stderr, console.FormatProgressMessage(fmt.Sprintf("Adding workflow %d/%d: %s", i+1, len(workflows), workflow.WorkflowName)))
}
@@ -451,12 +596,12 @@ func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, eng
currentName = name
}
- if err := addWorkflowWithTracking(workflow, number, verbose, engineOverride, currentName, force, appendText, tracker, fromWildcard, workflowDir, noStopAfter, stopAfter); err != nil {
+ if err := addWorkflowWithTracking(workflow, number, verbose, quiet, engineOverride, currentName, force, appendText, tracker, fromWildcard, workflowDir, noStopAfter, stopAfter); err != nil {
return fmt.Errorf("failed to add workflow '%s': %w", workflow.String(), err)
}
}
- if len(workflows) > 1 {
+ if !quiet && len(workflows) > 1 {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Successfully added all %d workflows", len(workflows))))
}
@@ -512,12 +657,12 @@ func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, eng
return nil
}
-// addWorkflowsWithPR handles workflow addition with PR creation
-func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, noGitattributes bool, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string) error {
+// addWorkflowsWithPR handles workflow addition with PR creation and returns the PR number and URL
+func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, quiet bool, engineOverride string, name string, force bool, appendText string, push bool, noGitattributes bool, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string) (int, string, error) {
// Get current branch for restoration later
currentBranch, err := getCurrentBranch()
if err != nil {
- return fmt.Errorf("failed to get current branch: %w", err)
+ return 0, "", fmt.Errorf("failed to get current branch: %w", err)
}
// Create temporary branch with random 4-digit number
@@ -525,13 +670,13 @@ func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, eng
branchName := fmt.Sprintf("add-workflow-%s-%04d", strings.ReplaceAll(workflows[0].WorkflowPath, "/", "-"), randomNum)
if err := createAndSwitchBranch(branchName, verbose); err != nil {
- return fmt.Errorf("failed to create branch %s: %w", branchName, err)
+ return 0, "", fmt.Errorf("failed to create branch %s: %w", branchName, err)
}
// Create file tracker for rollback capability
tracker, err := NewFileTracker()
if err != nil {
- return fmt.Errorf("failed to create file tracker: %w", err)
+ return 0, "", fmt.Errorf("failed to create file tracker: %w", err)
}
// Ensure we switch back to original branch on exit
@@ -542,12 +687,12 @@ func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, eng
}()
// Add workflows using the normal function logic
- if err := addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, false, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter); err != nil {
+ if err := addWorkflowsNormal(workflows, number, verbose, quiet, engineOverride, name, force, appendText, push, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter); err != nil {
// Rollback on error
if rollbackErr := tracker.RollbackAllFiles(verbose); rollbackErr != nil && verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to rollback files: %v", rollbackErr)))
}
- return fmt.Errorf("failed to add workflows: %w", err)
+ return 0, "", fmt.Errorf("failed to add workflows: %w", err)
}
// Stage all files before creating PR
@@ -555,7 +700,7 @@ func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, eng
if rollbackErr := tracker.RollbackAllFiles(verbose); rollbackErr != nil && verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to rollback files: %v", rollbackErr)))
}
- return fmt.Errorf("failed to stage workflow files: %w", err)
+ return 0, "", fmt.Errorf("failed to stage workflow files: %w", err)
}
// Update .gitattributes and stage it if modified
@@ -586,7 +731,7 @@ func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, eng
if rollbackErr := tracker.RollbackAllFiles(verbose); rollbackErr != nil && verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to rollback files: %v", rollbackErr)))
}
- return fmt.Errorf("failed to commit files: %w", err)
+ return 0, "", fmt.Errorf("failed to commit files: %w", err)
}
// Push branch
@@ -594,34 +739,31 @@ func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, eng
if rollbackErr := tracker.RollbackAllFiles(verbose); rollbackErr != nil && verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to rollback files: %v", rollbackErr)))
}
- return fmt.Errorf("failed to push branch %s: %w", branchName, err)
+ return 0, "", fmt.Errorf("failed to push branch %s: %w", branchName, err)
}
// Create PR
- if err := createPR(branchName, prTitle, prBody, verbose); err != nil {
+ prNumber, prURL, err := createPR(branchName, prTitle, prBody, verbose)
+ if err != nil {
if rollbackErr := tracker.RollbackAllFiles(verbose); rollbackErr != nil && verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to rollback files: %v", rollbackErr)))
}
- return fmt.Errorf("failed to create PR: %w", err)
+ return 0, "", fmt.Errorf("failed to create PR: %w", err)
}
// Success - no rollback needed
// Switch back to original branch
if err := switchBranch(currentBranch, verbose); err != nil {
- return fmt.Errorf("failed to switch back to branch %s: %w", currentBranch, err)
+ return prNumber, prURL, fmt.Errorf("failed to switch back to branch %s: %w", currentBranch, err)
}
- if len(workflows) == 1 {
- fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Created PR for workflow: %s", workflows[0].WorkflowName)))
- } else {
- fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Created PR for workflows: %s", joinedNames)))
- }
- return nil
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Created pull request %s", prURL)))
+ return prNumber, prURL, nil
}
// addWorkflowWithTracking adds a workflow from components to .github/workflows with file tracking
-func addWorkflowWithTracking(workflow *WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, tracker *FileTracker, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string) error {
+func addWorkflowWithTracking(workflow *WorkflowSpec, number int, verbose bool, quiet bool, engineOverride string, name string, force bool, appendText string, tracker *FileTracker, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string) error {
if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Adding workflow: %s", workflow.String())))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Number of copies: %d", number)))
@@ -848,23 +990,26 @@ func addWorkflowWithTracking(workflow *WorkflowSpec, number int, verbose bool, e
return fmt.Errorf("failed to write destination file '%s': %w", destFile, err)
}
- fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Added workflow: %s", destFile)))
+ // Show detailed output only when not in quiet mode
+ if !quiet {
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Added workflow: %s", destFile)))
- // Extract and display description if present
- if description := ExtractWorkflowDescription(content); description != "" {
- fmt.Fprintln(os.Stderr, "")
- fmt.Fprintln(os.Stderr, console.FormatInfoMessage(description))
- fmt.Fprintln(os.Stderr, "")
+ // Extract and display description if present
+ if description := ExtractWorkflowDescription(content); description != "" {
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, console.FormatInfoMessage(description))
+ fmt.Fprintln(os.Stderr, "")
+ }
}
// Try to compile the workflow and track generated files
if tracker != nil {
- if err := compileWorkflowWithTracking(destFile, verbose, engineOverride, tracker); err != nil {
+ if err := compileWorkflowWithTracking(destFile, verbose, quiet, engineOverride, tracker); err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
}
} else {
// Fall back to basic compilation without tracking
- if err := compileWorkflow(destFile, verbose, engineOverride); err != nil {
+ if err := compileWorkflow(destFile, verbose, quiet, engineOverride); err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
}
}
@@ -894,14 +1039,15 @@ func updateWorkflowTitle(content string, number int) string {
return strings.Join(lines, "\n")
}
-func compileWorkflow(filePath string, verbose bool, engineOverride string) error {
- return compileWorkflowWithRefresh(filePath, verbose, engineOverride, false)
+func compileWorkflow(filePath string, verbose bool, quiet bool, engineOverride string) error {
+ return compileWorkflowWithRefresh(filePath, verbose, quiet, engineOverride, false)
}
-func compileWorkflowWithRefresh(filePath string, verbose bool, engineOverride string, refreshStopTime bool) error {
+func compileWorkflowWithRefresh(filePath string, verbose bool, quiet bool, engineOverride string, refreshStopTime bool) error {
// Create compiler and compile the workflow
compiler := workflow.NewCompiler(verbose, engineOverride, GetVersion())
compiler.SetRefreshStopTime(refreshStopTime)
+ compiler.SetQuiet(quiet)
if err := CompileWorkflowWithValidation(compiler, filePath, verbose, false, false, false, false, false); err != nil {
return err
}
@@ -920,11 +1066,11 @@ func compileWorkflowWithRefresh(filePath string, verbose bool, engineOverride st
}
// compileWorkflowWithTracking compiles a workflow and tracks generated files
-func compileWorkflowWithTracking(filePath string, verbose bool, engineOverride string, tracker *FileTracker) error {
- return compileWorkflowWithTrackingAndRefresh(filePath, verbose, engineOverride, tracker, false)
+func compileWorkflowWithTracking(filePath string, verbose bool, quiet bool, engineOverride string, tracker *FileTracker) error {
+ return compileWorkflowWithTrackingAndRefresh(filePath, verbose, quiet, engineOverride, tracker, false)
}
-func compileWorkflowWithTrackingAndRefresh(filePath string, verbose bool, engineOverride string, tracker *FileTracker, refreshStopTime bool) error {
+func compileWorkflowWithTrackingAndRefresh(filePath string, verbose bool, quiet bool, engineOverride string, tracker *FileTracker, refreshStopTime bool) error {
// Generate the expected lock file path
lockFile := stringutil.MarkdownToLockFile(filePath)
@@ -963,6 +1109,7 @@ func compileWorkflowWithTrackingAndRefresh(filePath string, verbose bool, engine
compiler := workflow.NewCompiler(verbose, engineOverride, GetVersion())
compiler.SetFileTracker(tracker)
compiler.SetRefreshStopTime(refreshStopTime)
+ compiler.SetQuiet(quiet)
if err := CompileWorkflowWithValidation(compiler, filePath, verbose, false, false, false, false, false); err != nil {
return err
}
@@ -1020,3 +1167,52 @@ func expandWildcardWorkflows(specs []*WorkflowSpec, verbose bool) ([]*WorkflowSp
return expandedWorkflows, nil
}
+
+// checkWorkflowHasDispatch checks if a single workflow has a workflow_dispatch trigger
+func checkWorkflowHasDispatch(spec *WorkflowSpec, verbose bool) bool {
+ addLog.Printf("Checking if workflow %s has workflow_dispatch trigger", spec.WorkflowName)
+
+ // Find and read the workflow content
+ sourceContent, _, err := findWorkflowInPackageForRepo(spec, verbose)
+ if err != nil {
+ addLog.Printf("Could not fetch workflow content: %v", err)
+ return false
+ }
+
+ // Parse frontmatter to check on: triggers
+ result, err := parser.ExtractFrontmatterFromContent(string(sourceContent))
+ if err != nil {
+ addLog.Printf("Could not parse workflow frontmatter: %v", err)
+ return false
+ }
+
+ // Check if 'on' section exists and contains workflow_dispatch
+ onSection, exists := result.Frontmatter["on"]
+ if !exists {
+ addLog.Print("No 'on' section found in workflow")
+ return false
+ }
+
+ // Handle different on: formats
+ switch on := onSection.(type) {
+ case map[string]any:
+ _, hasDispatch := on["workflow_dispatch"]
+ addLog.Printf("workflow_dispatch in on map: %v", hasDispatch)
+ return hasDispatch
+ case string:
+ hasDispatch := strings.Contains(strings.ToLower(on), "workflow_dispatch")
+ addLog.Printf("workflow_dispatch in on string: %v", hasDispatch)
+ return hasDispatch
+ case []any:
+ for _, item := range on {
+ if str, ok := item.(string); ok && strings.ToLower(str) == "workflow_dispatch" {
+ addLog.Print("workflow_dispatch found in on array")
+ return true
+ }
+ }
+ return false
+ default:
+ addLog.Printf("Unknown on: section type: %T", onSection)
+ return false
+ }
+}
diff --git a/pkg/cli/add_command_test.go b/pkg/cli/add_command_test.go
index c6a6a69277..07c7d6313b 100644
--- a/pkg/cli/add_command_test.go
+++ b/pkg/cli/add_command_test.go
@@ -82,7 +82,7 @@ func TestNewAddCommand(t *testing.T) {
}
func TestAddWorkflows_EmptyWorkflows(t *testing.T) {
- err := AddWorkflows([]string{}, 1, false, "", "", false, "", false, false, false, "", false, "")
+ _, err := AddWorkflows([]string{}, 1, false, "", "", false, "", false, false, false, "", false, "")
require.Error(t, err, "Should error when no workflows are provided")
assert.Contains(t, err.Error(), "at least one workflow", "Error should mention missing workflow")
}
diff --git a/pkg/cli/add_current_repo_test.go b/pkg/cli/add_current_repo_test.go
index a77259fafa..ec53f83e5a 100644
--- a/pkg/cli/add_current_repo_test.go
+++ b/pkg/cli/add_current_repo_test.go
@@ -78,7 +78,7 @@ func TestAddWorkflowsFromCurrentRepository(t *testing.T) {
// Clear cache before each test
ClearCurrentRepoSlugCache()
- err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false, false, "", false, "")
+ _, err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false, false, "", false, "")
if tt.expectError {
if err == nil {
@@ -179,7 +179,7 @@ func TestAddWorkflowsFromCurrentRepositoryMultiple(t *testing.T) {
// Clear cache before each test
ClearCurrentRepoSlugCache()
- err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false, false, "", false, "")
+ _, err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false, false, "", false, "")
if tt.expectError {
if err == nil {
@@ -220,7 +220,7 @@ func TestAddWorkflowsFromCurrentRepositoryNotInGitRepo(t *testing.T) {
// When not in a git repo, the check should be skipped (can't determine current repo)
// The function should proceed and fail for other reasons (e.g., workflow not found)
- err = AddWorkflows([]string{"some-owner/some-repo/workflow"}, 1, false, "", "", false, "", false, false, false, "", false, "")
+ _, err = AddWorkflows([]string{"some-owner/some-repo/workflow"}, 1, false, "", "", false, "", false, false, false, "", false, "")
// Should NOT get the "cannot add workflows from the current repository" error
if err != nil && strings.Contains(err.Error(), "cannot add workflows from the current repository") {
diff --git a/pkg/cli/add_gitattributes_test.go b/pkg/cli/add_gitattributes_test.go
index 0b2f6f6f20..ab89ac8014 100644
--- a/pkg/cli/add_gitattributes_test.go
+++ b/pkg/cli/add_gitattributes_test.go
@@ -82,8 +82,7 @@ This is a test workflow.`
os.Remove(".gitattributes")
// Call addWorkflowsNormal with noGitattributes=false
- // Signature: addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, push, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter)
- err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", false, false, false, "", false, "")
+ err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, false, "", "", false, "", false, false, false, "", false, "")
if err != nil {
// We expect this to fail because we don't have a full workflow setup,
// but gitattributes should still be updated before the error
@@ -113,8 +112,7 @@ This is a test workflow.`
os.Remove(".gitattributes")
// Call addWorkflowsNormal with noGitattributes=true
- // Signature: addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, push, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter)
- err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", false, true, false, "", false, "")
+ err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, false, "", "", false, "", false, true, false, "", false, "")
if err != nil {
// We expect this to fail because we don't have a full workflow setup
t.Logf("Expected error during workflow addition: %v", err)
@@ -136,8 +134,7 @@ This is a test workflow.`
}
// Call addWorkflowsNormal with noGitattributes=true
- // Signature: addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, push, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter)
- err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", false, true, false, "", false, "")
+ err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, false, "", "", false, "", false, true, false, "", false, "")
if err != nil {
// We expect this to fail because we don't have a full workflow setup
t.Logf("Expected error during workflow addition: %v", err)
diff --git a/pkg/cli/add_interactive.go b/pkg/cli/add_interactive.go
new file mode 100644
index 0000000000..75a380efe1
--- /dev/null
+++ b/pkg/cli/add_interactive.go
@@ -0,0 +1,977 @@
+package cli
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+
+ "github.com/charmbracelet/huh"
+ "github.com/githubnext/gh-aw/pkg/console"
+ "github.com/githubnext/gh-aw/pkg/constants"
+ "github.com/githubnext/gh-aw/pkg/logger"
+ "github.com/githubnext/gh-aw/pkg/parser"
+ "github.com/githubnext/gh-aw/pkg/workflow"
+)
+
+var addInteractiveLog = logger.New("cli:add_interactive")
+
+// AddInteractiveConfig holds configuration for interactive add mode
+type AddInteractiveConfig struct {
+ WorkflowSpecs []string
+ Verbose bool
+ EngineOverride string
+ NoGitattributes bool
+ WorkflowDir string
+ NoStopAfter bool
+ StopAfter string
+ SkipWorkflowRun bool
+ RepoOverride string // owner/repo format, if user provides it
+
+ // isPublicRepo tracks whether the target repository is public
+ // This is populated by checkGitRepository() when determining the repo
+ isPublicRepo bool
+
+ // existingSecrets tracks which secrets already exist in the repository
+ // This is populated by checkExistingSecrets() before engine selection
+ existingSecrets map[string]bool
+
+ // addResult holds the result from AddWorkflows, including HasWorkflowDispatch
+ addResult *AddWorkflowsResult
+
+ // resolvedWorkflows holds the pre-resolved workflow data including descriptions
+ // This is populated early in the flow by resolveWorkflows()
+ resolvedWorkflows *ResolvedWorkflows
+}
+
+// RunAddInteractive runs the interactive add workflow
+// This walks the user through adding an agentic workflow to their repository
+func RunAddInteractive(ctx context.Context, workflowSpecs []string, verbose bool, engineOverride string, noGitattributes bool, workflowDir string, noStopAfter bool, stopAfter string) error {
+ addInteractiveLog.Print("Starting interactive add workflow")
+
+ // Assert this function is not running in automated unit tests or CI
+ if os.Getenv("GO_TEST_MODE") == "true" || os.Getenv("CI") != "" {
+ return fmt.Errorf("interactive add cannot be used in automated tests or CI environments")
+ }
+
+ config := &AddInteractiveConfig{
+ WorkflowSpecs: workflowSpecs,
+ Verbose: verbose,
+ EngineOverride: engineOverride,
+ NoGitattributes: noGitattributes,
+ WorkflowDir: workflowDir,
+ NoStopAfter: noStopAfter,
+ StopAfter: stopAfter,
+ }
+
+ // Clear the screen for a fresh interactive experience
+ fmt.Fprint(os.Stderr, "\033[H\033[2J")
+
+ // Step 1: Welcome message
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "π Welcome to GitHub Agentic Workflows!")
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "This tool will walk you through adding an automated workflow to your repository.")
+ fmt.Fprintln(os.Stderr, "")
+
+ // Step 1b: Resolve workflows early to get descriptions and validate specs
+ if err := config.resolveWorkflows(); err != nil {
+ return err
+ }
+
+ // Step 1c: Show workflow descriptions if available
+ config.showWorkflowDescriptions()
+
+ // Step 2: Check gh auth status
+ if err := config.checkGHAuthStatus(); err != nil {
+ return err
+ }
+
+ // Step 3: Check git repository and get org/repo
+ if err := config.checkGitRepository(); err != nil {
+ return err
+ }
+
+ // Step 4: Check GitHub Actions is enabled
+ if err := config.checkActionsEnabled(); err != nil {
+ return err
+ }
+
+ // Step 5: Check user permissions
+ if err := config.checkUserPermissions(); err != nil {
+ return err
+ }
+
+ // Step 6: Select coding agent and collect API key
+ if err := config.selectAIEngineAndKey(); err != nil {
+ return err
+ }
+
+ // Step 7: Determine files to add
+ filesToAdd, initFiles, err := config.determineFilesToAdd()
+ if err != nil {
+ return err
+ }
+
+ // Step 8: Confirm with user
+ secretName, secretValue, err := config.getSecretInfo()
+ if err != nil {
+ return err
+ }
+
+ if err := config.confirmChanges(filesToAdd, initFiles, secretName, secretValue); err != nil {
+ return err
+ }
+
+ // Step 9: Apply changes (create PR, merge, add secret)
+ if err := config.applyChanges(ctx, filesToAdd, initFiles, secretName, secretValue); err != nil {
+ return err
+ }
+
+ // Step 10: Check status and offer to run
+ if err := config.checkStatusAndOfferRun(ctx); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// resolveWorkflows resolves workflow specifications by installing repositories,
+// expanding wildcards, and fetching workflow content (including descriptions).
+// This is called early to show workflow information before the user commits to adding them.
+func (c *AddInteractiveConfig) resolveWorkflows() error {
+ addInteractiveLog.Print("Resolving workflows early for description display")
+
+ resolved, err := ResolveWorkflows(c.WorkflowSpecs, c.Verbose)
+ if err != nil {
+ return fmt.Errorf("failed to resolve workflows: %w", err)
+ }
+
+ c.resolvedWorkflows = resolved
+ return nil
+}
+
+// showWorkflowDescriptions displays the descriptions of resolved workflows
+func (c *AddInteractiveConfig) showWorkflowDescriptions() {
+ if c.resolvedWorkflows == nil || len(c.resolvedWorkflows.Workflows) == 0 {
+ return
+ }
+
+ // Show descriptions for all workflows that have one
+ for _, rw := range c.resolvedWorkflows.Workflows {
+ if rw.Description != "" {
+ fmt.Fprintln(os.Stderr, console.FormatInfoMessage(rw.Description))
+ fmt.Fprintln(os.Stderr, "")
+ }
+ }
+}
+
+// checkGHAuthStatus verifies the user is logged in to GitHub CLI
+func (c *AddInteractiveConfig) checkGHAuthStatus() error {
+ addInteractiveLog.Print("Checking GitHub CLI authentication status")
+
+ cmd := exec.Command("gh", "auth", "status")
+ output, err := cmd.CombinedOutput()
+
+ if err != nil {
+ fmt.Fprintln(os.Stderr, console.FormatErrorMessage("You are not logged in to GitHub CLI."))
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "Please run the following command to authenticate:")
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, console.FormatCommandMessage(" gh auth login"))
+ fmt.Fprintln(os.Stderr, "")
+ return fmt.Errorf("not authenticated with GitHub CLI")
+ }
+
+ if c.Verbose {
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("GitHub CLI authenticated"))
+ addInteractiveLog.Printf("gh auth status output: %s", string(output))
+ }
+
+ return nil
+}
+
+// checkGitRepository verifies we're in a git repo and gets org/repo info
+func (c *AddInteractiveConfig) checkGitRepository() error {
+ addInteractiveLog.Print("Checking git repository status")
+
+ // Check if we're in a git repository
+ if !isGitRepo() {
+ fmt.Fprintln(os.Stderr, console.FormatErrorMessage("Not in a git repository."))
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "Please navigate to a git repository or initialize one with:")
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, console.FormatCommandMessage(" git init"))
+ fmt.Fprintln(os.Stderr, "")
+ return fmt.Errorf("not in a git repository")
+ }
+
+ // Try to get the repository slug
+ repoSlug, err := GetCurrentRepoSlug()
+ if err != nil {
+ addInteractiveLog.Printf("Could not determine repository automatically: %v", err)
+
+ // Ask the user for the repository
+ fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Could not determine the repository automatically."))
+ fmt.Fprintln(os.Stderr, "")
+
+ var userRepo string
+ form := huh.NewForm(
+ huh.NewGroup(
+ huh.NewInput().
+ Title("Enter the target repository (owner/repo):").
+ Description("For example: myorg/myrepo").
+ Value(&userRepo).
+ Validate(func(s string) error {
+ parts := strings.Split(s, "/")
+ if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
+ return fmt.Errorf("please enter in format 'owner/repo'")
+ }
+ return nil
+ }),
+ ),
+ ).WithAccessible(console.IsAccessibleMode())
+
+ if err := form.Run(); err != nil {
+ return fmt.Errorf("failed to get repository info: %w", err)
+ }
+
+ c.RepoOverride = userRepo
+ repoSlug = userRepo
+ } else {
+ c.RepoOverride = repoSlug
+ }
+
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Target repository: %s", repoSlug)))
+ addInteractiveLog.Printf("Target repository: %s", repoSlug)
+
+ // Check if repository is public or private
+ c.isPublicRepo = c.checkRepoVisibility()
+
+ return nil
+}
+
+// checkRepoVisibility checks if the repository is public or private
+func (c *AddInteractiveConfig) checkRepoVisibility() bool {
+ addInteractiveLog.Print("Checking repository visibility")
+
+ // Use gh api to check repository visibility
+ args := []string{"api", fmt.Sprintf("/repos/%s", c.RepoOverride), "--jq", ".visibility"}
+ cmd := workflow.ExecGH(args...)
+ output, err := cmd.Output()
+ if err != nil {
+ addInteractiveLog.Printf("Could not check repository visibility: %v", err)
+ // Default to public if we can't determine
+ return true
+ }
+
+ visibility := strings.TrimSpace(string(output))
+ isPublic := visibility == "public"
+ addInteractiveLog.Printf("Repository visibility: %s (isPublic=%v)", visibility, isPublic)
+ return isPublic
+}
+
+// checkActionsEnabled verifies that GitHub Actions is enabled for the repository
+func (c *AddInteractiveConfig) checkActionsEnabled() error {
+ addInteractiveLog.Print("Checking if GitHub Actions is enabled")
+
+ // Use gh api to check Actions permissions
+ args := []string{"api", fmt.Sprintf("/repos/%s/actions/permissions", c.RepoOverride), "--jq", ".enabled"}
+ cmd := workflow.ExecGH(args...)
+ output, err := cmd.Output()
+ if err != nil {
+ addInteractiveLog.Printf("Failed to check Actions status: %v", err)
+ // If we can't check, warn but continue - actual operations will fail if Actions is disabled
+ fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Could not verify GitHub Actions status. Proceeding anyway..."))
+ return nil
+ }
+
+ enabled := strings.TrimSpace(string(output))
+ if enabled != "true" {
+ fmt.Fprintln(os.Stderr, console.FormatErrorMessage("GitHub Actions is disabled for this repository."))
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "To enable GitHub Actions:")
+ fmt.Fprintln(os.Stderr, " 1. Go to your repository on GitHub")
+ fmt.Fprintln(os.Stderr, " 2. Navigate to Settings β Actions β General")
+ fmt.Fprintln(os.Stderr, " 3. Under 'Actions permissions', select 'Allow all actions and reusable workflows'")
+ fmt.Fprintln(os.Stderr, " 4. Click 'Save'")
+ fmt.Fprintln(os.Stderr, "")
+ return fmt.Errorf("GitHub Actions is not enabled for this repository")
+ }
+
+ if c.Verbose {
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("GitHub Actions is enabled"))
+ }
+
+ return nil
+}
+
+// checkUserPermissions verifies the user has write/admin access
+func (c *AddInteractiveConfig) checkUserPermissions() error {
+ addInteractiveLog.Print("Checking user permissions")
+
+ parts := strings.Split(c.RepoOverride, "/")
+ if len(parts) != 2 {
+ return fmt.Errorf("invalid repository format: %s", c.RepoOverride)
+ }
+ owner, repo := parts[0], parts[1]
+
+ hasAccess, err := checkRepositoryAccess(owner, repo)
+ if err != nil {
+ addInteractiveLog.Printf("Failed to check repository access: %v", err)
+ // If we can't check, warn but continue - actual operations will fail if no access
+ fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Could not verify repository permissions. Proceeding anyway..."))
+ return nil
+ }
+
+ if !hasAccess {
+ fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("You do not have write access to %s/%s.", owner, repo)))
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "You need to be a maintainer, admin, or have write permissions on this repository.")
+ fmt.Fprintln(os.Stderr, "Please contact the repository owner or request access.")
+ fmt.Fprintln(os.Stderr, "")
+ return fmt.Errorf("insufficient repository permissions")
+ }
+
+ if c.Verbose {
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Repository permissions verified"))
+ }
+
+ return nil
+}
+
+// checkExistingSecrets fetches which secrets already exist in the repository
+func (c *AddInteractiveConfig) checkExistingSecrets() error {
+ addInteractiveLog.Print("Checking existing repository secrets")
+
+ c.existingSecrets = make(map[string]bool)
+
+ // Use gh api to list repository secrets
+ args := []string{"api", fmt.Sprintf("/repos/%s/actions/secrets", c.RepoOverride), "--jq", ".secrets[].name"}
+ cmd := workflow.ExecGH(args...)
+ output, err := cmd.Output()
+ if err != nil {
+ addInteractiveLog.Printf("Could not fetch existing secrets: %v", err)
+ // Continue without error - we'll just assume no secrets exist
+ return nil
+ }
+
+ // Parse the output - each secret name is on its own line
+ secretNames := strings.Split(strings.TrimSpace(string(output)), "\n")
+ for _, name := range secretNames {
+ name = strings.TrimSpace(name)
+ if name != "" {
+ c.existingSecrets[name] = true
+ addInteractiveLog.Printf("Found existing secret: %s", name)
+ }
+ }
+
+ if c.Verbose && len(c.existingSecrets) > 0 {
+ fmt.Fprintf(os.Stderr, "Found %d existing repository secret(s)\n", len(c.existingSecrets))
+ }
+
+ return nil
+}
+
+// selectAIEngineAndKey prompts the user to select an AI engine and provide API key
+func (c *AddInteractiveConfig) selectAIEngineAndKey() error {
+ addInteractiveLog.Print("Starting coding agent selection")
+
+ // First, check which secrets already exist in the repository
+ if err := c.checkExistingSecrets(); err != nil {
+ return err
+ }
+
+ // Determine default engine based on workflow preference, existing secrets, then environment
+ defaultEngine := string(constants.CopilotEngine)
+ existingSecretNote := ""
+
+ // If engine is explicitly overridden via flag, use that
+ if c.EngineOverride != "" {
+ defaultEngine = c.EngineOverride
+ } else {
+ // Priority 0: Check if workflow specifies a preferred engine in frontmatter
+ if c.resolvedWorkflows != nil && len(c.resolvedWorkflows.Workflows) > 0 {
+ for _, wf := range c.resolvedWorkflows.Workflows {
+ if wf.Engine != "" {
+ defaultEngine = wf.Engine
+ addInteractiveLog.Printf("Using engine from workflow frontmatter: %s", wf.Engine)
+ break
+ }
+ }
+ }
+ }
+
+ // Only check secrets/environment if we haven't already set a preference
+ workflowHasPreference := c.resolvedWorkflows != nil && len(c.resolvedWorkflows.Workflows) > 0 && c.resolvedWorkflows.Workflows[0].Engine != ""
+ if c.EngineOverride == "" && !workflowHasPreference {
+ // Priority 1: Check existing repository secrets using EngineOptions
+ for _, opt := range constants.EngineOptions {
+ if c.existingSecrets[opt.SecretName] {
+ defaultEngine = opt.Value
+ existingSecretNote = fmt.Sprintf(" (existing %s secret will be used)", opt.SecretName)
+ break
+ }
+ }
+
+ // Priority 2: Check environment variables if no existing secret found
+ if existingSecretNote == "" {
+ for _, opt := range constants.EngineOptions {
+ envVar := opt.SecretName
+ if opt.EnvVarName != "" {
+ envVar = opt.EnvVarName
+ }
+ if os.Getenv(envVar) != "" {
+ defaultEngine = opt.Value
+ break
+ }
+ }
+ // Priority 3: Check if user likely has Copilot (default)
+ if token, err := parser.GetGitHubToken(); err == nil && token != "" {
+ defaultEngine = string(constants.CopilotEngine)
+ }
+ }
+ }
+
+ // If engine is already overridden, skip selection
+ if c.EngineOverride != "" {
+ fmt.Fprintf(os.Stderr, "Using coding agent: %s\n", c.EngineOverride)
+ return c.collectAPIKey(c.EngineOverride)
+ }
+
+ // Build engine options with notes about existing secrets
+ var engineOptions []huh.Option[string]
+ for _, opt := range constants.EngineOptions {
+ label := fmt.Sprintf("%s - %s", opt.Label, opt.Description)
+ if c.existingSecrets[opt.SecretName] {
+ label += " [secret exists]"
+ }
+ engineOptions = append(engineOptions, huh.NewOption(label, opt.Value))
+ }
+
+ var selectedEngine string
+
+ // Set the default selection by moving it to front
+ for i, opt := range engineOptions {
+ if opt.Value == defaultEngine {
+ if i > 0 {
+ engineOptions[0], engineOptions[i] = engineOptions[i], engineOptions[0]
+ }
+ break
+ }
+ }
+
+ fmt.Fprintln(os.Stderr, "")
+ form := huh.NewForm(
+ huh.NewGroup(
+ huh.NewSelect[string]().
+ Title("Which coding agent would you like to use?").
+ Description("This determines which coding agent processes your workflows").
+ Options(engineOptions...).
+ Value(&selectedEngine),
+ ),
+ ).WithAccessible(console.IsAccessibleMode())
+
+ if err := form.Run(); err != nil {
+ return fmt.Errorf("failed to select coding agent: %w", err)
+ }
+
+ c.EngineOverride = selectedEngine
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Selected engine: %s", selectedEngine)))
+
+ return c.collectAPIKey(selectedEngine)
+}
+
+// collectAPIKey collects the API key for the selected engine
+func (c *AddInteractiveConfig) collectAPIKey(engine string) error {
+ addInteractiveLog.Printf("Collecting API key for engine: %s", engine)
+
+ // Copilot requires special handling with PAT creation instructions
+ if engine == "copilot" {
+ return c.collectCopilotPAT()
+ }
+
+ // All other engines use the generic API key collection
+ opt := constants.GetEngineOption(engine)
+ if opt == nil {
+ return fmt.Errorf("unknown engine: %s", engine)
+ }
+
+ return c.collectGenericAPIKey(opt)
+}
+
+// collectCopilotPAT walks the user through creating a Copilot PAT
+func (c *AddInteractiveConfig) collectCopilotPAT() error {
+ addInteractiveLog.Print("Collecting Copilot PAT")
+
+ // Check if secret already exists in the repository
+ if c.existingSecrets["COPILOT_GITHUB_TOKEN"] {
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Using existing COPILOT_GITHUB_TOKEN secret in repository"))
+ return nil
+ }
+
+ // Check if COPILOT_GITHUB_TOKEN is already in environment
+ existingToken := os.Getenv("COPILOT_GITHUB_TOKEN")
+ if existingToken != "" {
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Found COPILOT_GITHUB_TOKEN in environment"))
+ return nil
+ }
+
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "GitHub Copilot requires a Personal Access Token (PAT) with Copilot permissions.")
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "Please create a token at:")
+ fmt.Fprintln(os.Stderr, console.FormatCommandMessage(" https://github.com/settings/personal-access-tokens/new"))
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "Configure the token with:")
+ fmt.Fprintln(os.Stderr, " β’ Token name: Agentic Workflows Copilot")
+ fmt.Fprintln(os.Stderr, " β’ Expiration: 90 days (recommended for testing)")
+ fmt.Fprintln(os.Stderr, " β’ Resource owner: Your personal account")
+ if c.isPublicRepo {
+ fmt.Fprintln(os.Stderr, " β’ Repository access: \"Public repositories\"")
+ } else {
+ fmt.Fprintf(os.Stderr, " β’ Repository access: \"Only select repositories\" β select %s\n", c.RepoOverride)
+ }
+ fmt.Fprintln(os.Stderr, " β’ Account permissions β Copilot Requests: Read")
+ fmt.Fprintln(os.Stderr, "")
+
+ var token string
+ form := huh.NewForm(
+ huh.NewGroup(
+ huh.NewInput().
+ Title("After creating, please paste your Copilot PAT:").
+ Description("The token will be stored securely as a repository secret").
+ EchoMode(huh.EchoModePassword).
+ Value(&token).
+ Validate(func(s string) error {
+ if len(s) < 10 {
+ return fmt.Errorf("token appears to be too short")
+ }
+ return nil
+ }),
+ ),
+ ).WithAccessible(console.IsAccessibleMode())
+
+ if err := form.Run(); err != nil {
+ return fmt.Errorf("failed to get Copilot token: %w", err)
+ }
+
+ // Store in environment for later use
+ os.Setenv("COPILOT_GITHUB_TOKEN", token)
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Copilot token received"))
+
+ return nil
+}
+
+// collectGenericAPIKey collects an API key for engines that use a simple key-based authentication
+func (c *AddInteractiveConfig) collectGenericAPIKey(opt *constants.EngineOption) error {
+ addInteractiveLog.Printf("Collecting API key for %s", opt.Label)
+
+ // Check if secret already exists in the repository
+ if c.existingSecrets[opt.SecretName] {
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Using existing %s secret in repository", opt.SecretName)))
+ return nil
+ }
+
+ // Check if key is already in environment
+ envVar := opt.SecretName
+ if opt.EnvVarName != "" {
+ envVar = opt.EnvVarName
+ }
+ existingKey := os.Getenv(envVar)
+ if existingKey != "" {
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Found %s in environment", envVar)))
+ return nil
+ }
+
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintf(os.Stderr, "%s requires an API key.\n", opt.Label)
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "Get your API key from:")
+ fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf(" %s", opt.KeyURL)))
+ fmt.Fprintln(os.Stderr, "")
+
+ var apiKey string
+ form := huh.NewForm(
+ huh.NewGroup(
+ huh.NewInput().
+ Title(fmt.Sprintf("Paste your %s API key:", opt.Label)).
+ Description("The key will be stored securely as a repository secret").
+ EchoMode(huh.EchoModePassword).
+ Value(&apiKey).
+ Validate(func(s string) error {
+ if len(s) < 10 {
+ return fmt.Errorf("API key appears to be too short")
+ }
+ return nil
+ }),
+ ),
+ ).WithAccessible(console.IsAccessibleMode())
+
+ if err := form.Run(); err != nil {
+ return fmt.Errorf("failed to get %s API key: %w", opt.Label, err)
+ }
+
+ // Store in environment for later use
+ os.Setenv(opt.SecretName, apiKey)
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("%s API key received", opt.Label)))
+
+ return nil
+}
+
+// determineFilesToAdd determines which files will be added
+func (c *AddInteractiveConfig) determineFilesToAdd() (workflowFiles []string, initFiles []string, err error) {
+ addInteractiveLog.Print("Determining files to add")
+
+ // Parse the workflow specs to get the files that will be added
+ // This reuses logic from addWorkflowsNormal to determine what files get created
+ for _, spec := range c.WorkflowSpecs {
+ parsed, parseErr := parseWorkflowSpec(spec)
+ if parseErr != nil {
+ return nil, nil, fmt.Errorf("invalid workflow specification '%s': %w", spec, parseErr)
+ }
+ workflowFiles = append(workflowFiles, parsed.WorkflowName+".md")
+ workflowFiles = append(workflowFiles, parsed.WorkflowName+".lock.yml")
+ }
+
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "The following workflow files will be added:")
+ for _, f := range workflowFiles {
+ fmt.Fprintf(os.Stderr, " β’ .github/workflows/%s\n", f)
+ }
+
+ return workflowFiles, initFiles, nil
+}
+
+// getSecretInfo returns the secret name and value based on the selected engine
+// Returns empty value if the secret already exists in the repository
+func (c *AddInteractiveConfig) getSecretInfo() (name string, value string, err error) {
+ addInteractiveLog.Printf("Getting secret info for engine: %s", c.EngineOverride)
+
+ opt := constants.GetEngineOption(c.EngineOverride)
+ if opt == nil {
+ return "", "", fmt.Errorf("unknown engine: %s", c.EngineOverride)
+ }
+
+ name = opt.SecretName
+
+ // If secret already exists in repo, we don't need a value
+ if c.existingSecrets[name] {
+ addInteractiveLog.Printf("Secret %s already exists in repository", name)
+ return name, "", nil
+ }
+
+ // Get value from environment variable (use EnvVarName if specified, otherwise SecretName)
+ envVar := opt.SecretName
+ if opt.EnvVarName != "" {
+ envVar = opt.EnvVarName
+ }
+ value = os.Getenv(envVar)
+
+ if value == "" {
+ return "", "", fmt.Errorf("API key not found for engine %s", c.EngineOverride)
+ }
+
+ return name, value, nil
+}
+
+// confirmChanges asks the user to confirm the changes
+// secretValue is empty if the secret already exists in the repository
+func (c *AddInteractiveConfig) confirmChanges(workflowFiles, initFiles []string, secretName string, secretValue string) error {
+ addInteractiveLog.Print("Confirming changes with user")
+
+ fmt.Fprintln(os.Stderr, "")
+
+ confirmed := true // Default to yes
+ form := huh.NewForm(
+ huh.NewGroup(
+ huh.NewConfirm().
+ Title("Do you want to proceed with these changes?").
+ Description("A pull request will be created and merged automatically").
+ Affirmative("Yes, create and merge").
+ Negative("No, cancel").
+ Value(&confirmed),
+ ),
+ ).WithAccessible(console.IsAccessibleMode())
+
+ if err := form.Run(); err != nil {
+ return fmt.Errorf("confirmation failed: %w", err)
+ }
+
+ if !confirmed {
+ fmt.Fprintln(os.Stderr, "Operation cancelled.")
+ return fmt.Errorf("user cancelled the operation")
+ }
+
+ return nil
+}
+
+// applyChanges creates the PR, merges it, and adds the secret
+func (c *AddInteractiveConfig) applyChanges(ctx context.Context, workflowFiles, initFiles []string, secretName, secretValue string) error {
+ addInteractiveLog.Print("Applying changes")
+
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, console.FormatProgressMessage("Creating pull request..."))
+
+ // Add the workflow using existing implementation with --create-pull-request
+ // Pass the resolved workflows to avoid re-fetching them
+ // Pass quiet=true to suppress detailed output (already shown earlier in interactive mode)
+ // This returns the result including PR number and HasWorkflowDispatch
+ result, err := AddResolvedWorkflows(c.WorkflowSpecs, c.resolvedWorkflows, 1, c.Verbose, true, c.EngineOverride, "", false, "", true, false, c.NoGitattributes, c.WorkflowDir, c.NoStopAfter, c.StopAfter)
+ if err != nil {
+ return fmt.Errorf("failed to add workflow: %w", err)
+ }
+ c.addResult = result
+
+ // Step 8b: Auto-merge the PR
+ fmt.Fprintln(os.Stderr, console.FormatProgressMessage("Merging pull request..."))
+
+ if result.PRNumber == 0 {
+ fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Could not determine PR number"))
+ fmt.Fprintln(os.Stderr, "Please merge the PR manually from the GitHub web interface.")
+ } else {
+ if err := c.mergePullRequest(result.PRNumber); err != nil {
+ // Check if already merged
+ if strings.Contains(err.Error(), "already merged") || strings.Contains(err.Error(), "MERGED") {
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Merged pull request %s", result.PRURL)))
+ } else {
+ fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to merge PR: %v", err)))
+ fmt.Fprintln(os.Stderr, "Please merge the PR manually from the GitHub web interface.")
+ }
+ } else {
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Merged pull request %s", result.PRURL)))
+ }
+ }
+
+ // Step 8c: Add the secret (skip if already exists in repository)
+ if secretValue == "" {
+ // Secret already exists in repo, nothing to do
+ if c.Verbose {
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Secret '%s' already configured", secretName)))
+ }
+ } else {
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, console.FormatProgressMessage(fmt.Sprintf("Adding secret '%s' to repository...", secretName)))
+
+ if err := c.addRepositorySecret(secretName, secretValue); err != nil {
+ fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to add secret: %v", err)))
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "Please add the secret manually:")
+ fmt.Fprintln(os.Stderr, " 1. Go to your repository Settings β Secrets and variables β Actions")
+ fmt.Fprintf(os.Stderr, " 2. Click 'New repository secret' and add '%s'\n", secretName)
+ return fmt.Errorf("failed to add secret: %w", err)
+ }
+
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Secret '%s' added", secretName)))
+ }
+
+ return nil
+}
+
+// mergePullRequest merges the specified PR
+func (c *AddInteractiveConfig) mergePullRequest(prNumber int) error {
+ cmd := workflow.ExecGH("pr", "merge", fmt.Sprintf("%d", prNumber), "--repo", c.RepoOverride, "--merge")
+ if output, err := cmd.CombinedOutput(); err != nil {
+ return fmt.Errorf("merge failed: %w (output: %s)", err, string(output))
+ }
+ return nil
+}
+
+// addRepositorySecret adds a secret to the repository
+func (c *AddInteractiveConfig) addRepositorySecret(name, value string) error {
+ cmd := workflow.ExecGH("secret", "set", name, "--repo", c.RepoOverride, "--body", value)
+ if output, err := cmd.CombinedOutput(); err != nil {
+ return fmt.Errorf("failed to set secret: %w (output: %s)", err, string(output))
+ }
+ return nil
+}
+
+// checkStatusAndOfferRun checks if the workflow appears in status and offers to run it
+func (c *AddInteractiveConfig) checkStatusAndOfferRun(ctx context.Context) error {
+ addInteractiveLog.Print("Checking workflow status and offering to run")
+
+ // Wait a moment for GitHub to process the merge
+ fmt.Fprintln(os.Stderr, "")
+
+ // Use spinner only in non-verbose mode (spinner can't be restarted after stop)
+ var spinner *console.SpinnerWrapper
+ if !c.Verbose {
+ spinner = console.NewSpinner("Waiting for workflow to be available...")
+ spinner.Start()
+ }
+
+ // Try a few times to see the workflow in status
+ var workflowFound bool
+ for i := 0; i < 5; i++ {
+ // Wait 2 seconds before each check (including the first)
+ select {
+ case <-ctx.Done():
+ if spinner != nil {
+ spinner.Stop()
+ }
+ return ctx.Err()
+ case <-time.After(2 * time.Second):
+ // Continue with check
+ }
+
+ // Use the workflow name from the first spec
+ if len(c.WorkflowSpecs) > 0 {
+ parsed, _ := parseWorkflowSpec(c.WorkflowSpecs[0])
+ if parsed != nil {
+ if c.Verbose {
+ fmt.Fprintf(os.Stderr, "Checking workflow status (attempt %d/5) for: %s\n", i+1, parsed.WorkflowName)
+ }
+ // Check if workflow is in status
+ statuses, err := getWorkflowStatuses(parsed.WorkflowName, c.RepoOverride, c.Verbose)
+ if err != nil {
+ if c.Verbose {
+ fmt.Fprintf(os.Stderr, "Status check error: %v\n", err)
+ }
+ } else if len(statuses) > 0 {
+ if c.Verbose {
+ fmt.Fprintf(os.Stderr, "Found %d workflow(s) matching pattern\n", len(statuses))
+ }
+ workflowFound = true
+ break
+ } else if c.Verbose {
+ fmt.Fprintln(os.Stderr, "No workflows found matching pattern yet")
+ }
+ }
+ }
+ }
+
+ if spinner != nil {
+ spinner.Stop()
+ }
+
+ if !workflowFound {
+ fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Could not verify workflow status."))
+ fmt.Fprintf(os.Stderr, "You can check status with: %s status\n", string(constants.CLIExtensionPrefix))
+ c.showFinalInstructions()
+ return nil
+ }
+
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Workflow is ready"))
+
+ // Only offer to run if workflow has workflow_dispatch trigger
+ if c.addResult == nil || !c.addResult.HasWorkflowDispatch {
+ addInteractiveLog.Print("Workflow does not have workflow_dispatch trigger, skipping run offer")
+ c.showFinalInstructions()
+ return nil
+ }
+
+ // Ask if user wants to run the workflow
+ fmt.Fprintln(os.Stderr, "")
+ runNow := true // Default to yes
+ form := huh.NewForm(
+ huh.NewGroup(
+ huh.NewConfirm().
+ Title("Would you like to run the workflow once now?").
+ Description("This will trigger the workflow immediately").
+ Affirmative("Yes, run once now").
+ Negative("No, I'll run later").
+ Value(&runNow),
+ ),
+ ).WithAccessible(console.IsAccessibleMode())
+
+ if err := form.Run(); err != nil {
+ return nil // Not critical, just skip
+ }
+
+ if !runNow {
+ c.showFinalInstructions()
+ return nil
+ }
+
+ // Run the workflow
+ if len(c.WorkflowSpecs) > 0 {
+ parsed, _ := parseWorkflowSpec(c.WorkflowSpecs[0])
+ if parsed != nil {
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, console.FormatProgressMessage("Triggering workflow..."))
+
+ if err := RunWorkflowOnGitHub(ctx, parsed.WorkflowName, false, c.EngineOverride, c.RepoOverride, "", false, false, false, true, nil, c.Verbose); err != nil {
+ fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to run workflow: %v", err)))
+ c.showFinalInstructions()
+ return nil
+ }
+
+ // Get the run URL for step 10
+ runInfo, err := getLatestWorkflowRunWithRetry(parsed.WorkflowName+".lock.yml", c.RepoOverride, c.Verbose)
+ if err == nil && runInfo.URL != "" {
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Workflow triggered successfully!"))
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintf(os.Stderr, "π View workflow run: %s\n", runInfo.URL)
+ }
+ }
+ }
+
+ c.showFinalInstructions()
+ return nil
+}
+
+// getWorkflowStatuses is a helper to get workflow statuses for a pattern
+// The pattern is matched against the workflow filename (basename without extension)
+func getWorkflowStatuses(pattern, repoOverride string, verbose bool) ([]WorkflowStatus, error) {
+ // This would normally call StatusWorkflows but we need just a simple check
+ // For now, we'll use the gh CLI directly
+ // Request 'path' field so we can match by filename, not by workflow name
+ args := []string{"workflow", "list", "--json", "name,state,path"}
+ if repoOverride != "" {
+ args = append(args, "--repo", repoOverride)
+ }
+
+ if verbose {
+ fmt.Fprintf(os.Stderr, "Running: gh %s\n", strings.Join(args, " "))
+ }
+
+ cmd := workflow.ExecGH(args...)
+ output, err := cmd.Output()
+ if err != nil {
+ if verbose {
+ fmt.Fprintf(os.Stderr, "gh workflow list failed: %v\n", err)
+ }
+ return nil, err
+ }
+
+ if verbose {
+ fmt.Fprintf(os.Stderr, "gh workflow list output: %s\n", string(output))
+ fmt.Fprintf(os.Stderr, "Looking for workflow with filename containing: %s\n", pattern)
+ }
+
+ // Check if any workflow path contains the pattern
+ // The pattern is the workflow name (e.g., "daily-repo-status")
+ // The path is like ".github/workflows/daily-repo-status.lock.yml"
+ // We check if the path contains the pattern
+ if strings.Contains(string(output), pattern+".lock.yml") || strings.Contains(string(output), pattern+".md") {
+ if verbose {
+ fmt.Fprintf(os.Stderr, "Workflow with filename '%s' found in workflow list\n", pattern)
+ }
+ return []WorkflowStatus{{Workflow: pattern}}, nil
+ }
+
+ if verbose {
+ fmt.Fprintf(os.Stderr, "Workflow with filename '%s' NOT found in workflow list\n", pattern)
+ }
+ return nil, nil
+}
+
+// showFinalInstructions shows final instructions to the user
+func (c *AddInteractiveConfig) showFinalInstructions() {
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "ββββββββββββββββββββββββββββββββββββββββ")
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("π Addition complete!"))
+ fmt.Fprintln(os.Stderr, "ββββββββββββββββββββββββββββββββββββββββ")
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "Useful commands:")
+ fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf(" %s status # Check workflow status", string(constants.CLIExtensionPrefix))))
+ fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf(" %s run # Trigger a workflow", string(constants.CLIExtensionPrefix))))
+ fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf(" %s logs # View workflow logs", string(constants.CLIExtensionPrefix))))
+ fmt.Fprintln(os.Stderr, "")
+ fmt.Fprintln(os.Stderr, "Learn more at: https://githubnext.github.io/gh-aw/")
+ fmt.Fprintln(os.Stderr, "")
+}
diff --git a/pkg/cli/add_wildcard_test.go b/pkg/cli/add_wildcard_test.go
index db6c3ab338..b4e9f0d1c2 100644
--- a/pkg/cli/add_wildcard_test.go
+++ b/pkg/cli/add_wildcard_test.go
@@ -475,7 +475,7 @@ on: push
// Test 1: Non-wildcard duplicate should return error
t.Run("non_wildcard_duplicate_returns_error", func(t *testing.T) {
- err := addWorkflowWithTracking(spec, 1, false, "", "", false, "", nil, false, "", false, "")
+ err := addWorkflowWithTracking(spec, 1, false, false, "", "", false, "", nil, false, "", false, "")
if err == nil {
t.Error("Expected error for non-wildcard duplicate, got nil")
}
@@ -486,7 +486,7 @@ on: push
// Test 2: Wildcard duplicate should return nil (skip with warning)
t.Run("wildcard_duplicate_returns_nil", func(t *testing.T) {
- err := addWorkflowWithTracking(spec, 1, false, "", "", false, "", nil, true, "", false, "")
+ err := addWorkflowWithTracking(spec, 1, false, false, "", "", false, "", nil, true, "", false, "")
if err != nil {
t.Errorf("Expected nil for wildcard duplicate (should skip), got error: %v", err)
}
@@ -494,7 +494,7 @@ on: push
// Test 3: Wildcard duplicate with force flag should succeed
t.Run("wildcard_duplicate_with_force_succeeds", func(t *testing.T) {
- err := addWorkflowWithTracking(spec, 1, false, "", "", true, "", nil, true, "", false, "")
+ err := addWorkflowWithTracking(spec, 1, false, false, "", "", true, "", nil, true, "", false, "")
// This should succeed or return nil
if err != nil && strings.Contains(err.Error(), "already exists") {
t.Errorf("Expected success with force flag, got 'already exists' error: %v", err)
diff --git a/pkg/cli/commands_compile_workflow_test.go b/pkg/cli/commands_compile_workflow_test.go
index bb281bbafc..980d4c28d2 100644
--- a/pkg/cli/commands_compile_workflow_test.go
+++ b/pkg/cli/commands_compile_workflow_test.go
@@ -223,7 +223,7 @@ Test compilation with invalid engine.
}
// Test compileWorkflow function
- err = compileWorkflow(workflowFile, tt.verbose, tt.engineOverride)
+ err = compileWorkflow(workflowFile, tt.verbose, false, tt.engineOverride)
if tt.expectError {
if err == nil {
diff --git a/pkg/cli/enable.go b/pkg/cli/enable.go
index 74c6c06805..2c279d4a99 100644
--- a/pkg/cli/enable.go
+++ b/pkg/cli/enable.go
@@ -139,7 +139,7 @@ func toggleWorkflowsByNames(workflowNames []string, enable bool, repoOverride st
// If enabling and lock file doesn't exist locally, try to compile it
if enable {
if _, err := os.Stat(lockFile); os.IsNotExist(err) {
- if err := compileWorkflow(file, false, ""); err != nil {
+ if err := compileWorkflow(file, false, false, ""); err != nil {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to compile workflow %s to create lock file: %v", name, err)))
// If we can't compile and there's no GitHub entry, skip because we can't address it
if !exists {
diff --git a/pkg/cli/file_tracker_test.go b/pkg/cli/file_tracker_test.go
index a85e565ce3..79e171e7b9 100644
--- a/pkg/cli/file_tracker_test.go
+++ b/pkg/cli/file_tracker_test.go
@@ -304,7 +304,7 @@ This uses reaction.
}
// Compile the workflow with tracking
- if err := compileWorkflowWithTracking(workflowFileWithReaction, false, "", tracker); err != nil {
+ if err := compileWorkflowWithTracking(workflowFileWithReaction, false, false, "", tracker); err != nil {
t.Fatalf("Failed to compile workflow: %v", err)
}
@@ -355,7 +355,7 @@ This does NOT use ai-reaction.
// (Note: Since reaction is now inline, this removal step is no longer needed)
// Compile the workflow with tracking
- if err := compileWorkflowWithTracking(workflowFileWithoutReaction, false, "", tracker2); err != nil {
+ if err := compileWorkflowWithTracking(workflowFileWithoutReaction, false, false, "", tracker2); err != nil {
t.Fatalf("Failed to compile workflow: %v", err)
}
diff --git a/pkg/cli/init.go b/pkg/cli/init.go
index 5bc2b3749c..2356f1f877 100644
--- a/pkg/cli/init.go
+++ b/pkg/cli/init.go
@@ -39,18 +39,9 @@ func InitRepositoryInteractive(verbose bool, rootCmd CommandProvider) error {
// Prompt for engine selection
var selectedEngine string
- engineOptions := []struct {
- value string
- label string
- description string
- }{
- {string(constants.CopilotEngine), "GitHub Copilot", "GitHub Copilot CLI with agent support"},
- {string(constants.ClaudeEngine), "Claude", "Anthropic Claude Code coding agent"},
- {string(constants.CodexEngine), "Codex", "OpenAI Codex/GPT engine"},
- }
// Use interactive prompt to select engine
- form := createEngineSelectionForm(&selectedEngine, engineOptions)
+ form := createEngineSelectionForm(&selectedEngine, constants.EngineOptions)
if err := form.Run(); err != nil {
return fmt.Errorf("engine selection failed: %w", err)
}
@@ -138,22 +129,18 @@ func InitRepositoryInteractive(verbose bool, rootCmd CommandProvider) error {
}
// createEngineSelectionForm creates an interactive form for engine selection
-func createEngineSelectionForm(selectedEngine *string, engineOptions []struct {
- value string
- label string
- description string
-}) *huh.Form {
+func createEngineSelectionForm(selectedEngine *string, engineOptions []constants.EngineOption) *huh.Form {
// Build options for huh.Select
var options []huh.Option[string]
for _, opt := range engineOptions {
- options = append(options, huh.NewOption(fmt.Sprintf("%s - %s", opt.label, opt.description), opt.value))
+ options = append(options, huh.NewOption(fmt.Sprintf("%s - %s", opt.Label, opt.Description), opt.Value))
}
return huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
- Title("Which AI engine would you like to use?").
- Description("Select the AI engine that will power your agentic workflows").
+ Title("Which coding agent would you like to use?").
+ Description("Select the coding agent that will power your agentic workflows").
Options(options...).
Value(selectedEngine),
),
@@ -732,7 +719,7 @@ func InitRepository(verbose bool, mcp bool, campaign bool, tokens bool, engine s
"- Configuring .gitattributes\n" +
"- Creating GitHub Copilot custom instructions\n" +
"- Setting up workflow prompts and agents"
- if err := createPR(branchName, prTitle, prBody, verbose); err != nil {
+ if _, _, err := createPR(branchName, prTitle, prBody, verbose); err != nil {
// Switch back to original branch before returning error
_ = switchBranch(currentBranch, verbose)
return fmt.Errorf("failed to create PR: %w", err)
diff --git a/pkg/cli/packages.go b/pkg/cli/packages.go
index 93970dc9eb..90553d6cb0 100644
--- a/pkg/cli/packages.go
+++ b/pkg/cli/packages.go
@@ -80,7 +80,7 @@ func InstallPackage(repoSpec string, verbose bool) error {
if _, err := os.Stat(targetDir); err == nil {
entries, err := os.ReadDir(targetDir)
if err == nil && len(entries) > 0 {
- fmt.Fprintf(os.Stderr, "Package %s already exists. Updating...\n", spec.RepoSlug)
+ packagesLog.Printf("Package %s already exists. Updating...\n", spec.RepoSlug)
// Remove existing content
if err := os.RemoveAll(targetDir); err != nil {
return fmt.Errorf("failed to remove existing package: %w", err)
@@ -99,7 +99,6 @@ func InstallPackage(repoSpec string, verbose bool) error {
}
packagesLog.Printf("Successfully installed package: %s", spec.RepoSlug)
- fmt.Fprintf(os.Stderr, "Successfully installed package: %s\n", spec.RepoSlug)
return nil
}
@@ -798,6 +797,32 @@ func ExtractWorkflowDescription(content string) string {
return ""
}
+// ExtractWorkflowEngine extracts the engine field from workflow content string.
+// Supports both string format (engine: copilot) and nested format (engine: { id: copilot }).
+func ExtractWorkflowEngine(content string) string {
+ result, err := parser.ExtractFrontmatterFromContent(content)
+ if err != nil {
+ return ""
+ }
+
+ if engine, ok := result.Frontmatter["engine"]; ok {
+ // Handle string format: engine: copilot
+ if engineStr, ok := engine.(string); ok {
+ return engineStr
+ }
+ // Handle nested format: engine: { id: copilot }
+ if engineMap, ok := engine.(map[string]any); ok {
+ if id, ok := engineMap["id"]; ok {
+ if idStr, ok := id.(string); ok {
+ return idStr
+ }
+ }
+ }
+ }
+
+ return ""
+}
+
// ExtractWorkflowDescriptionFromFile extracts the description field from a workflow file
func ExtractWorkflowDescriptionFromFile(filePath string) string {
content, err := os.ReadFile(filePath)
diff --git a/pkg/cli/pr_command.go b/pkg/cli/pr_command.go
index e4d0ebf31e..3c9216f9f1 100644
--- a/pkg/cli/pr_command.go
+++ b/pkg/cli/pr_command.go
@@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"path/filepath"
+ "strconv"
"strings"
"time"
@@ -788,8 +789,8 @@ func transferPR(prURL, targetRepo string, verbose bool) error {
return nil
}
-// createPR creates a pull request using GitHub CLI
-func createPR(branchName, title, body string, verbose bool) error {
+// createPR creates a pull request using GitHub CLI and returns the PR number
+func createPR(branchName, title, body string, verbose bool) (int, string, error) {
if verbose {
fmt.Printf("Creating PR: %s\n", title)
}
@@ -798,7 +799,7 @@ func createPR(branchName, title, body string, verbose bool) error {
cmd := workflow.ExecGH("repo", "view", "--json", "owner,name")
repoOutput, err := cmd.Output()
if err != nil {
- return fmt.Errorf("failed to get current repository info: %w", err)
+ return 0, "", fmt.Errorf("failed to get current repository info: %w", err)
}
var repoInfo struct {
@@ -809,7 +810,7 @@ func createPR(branchName, title, body string, verbose bool) error {
}
if err := json.Unmarshal(repoOutput, &repoInfo); err != nil {
- return fmt.Errorf("failed to parse repository info: %w", err)
+ return 0, "", fmt.Errorf("failed to parse repository info: %w", err)
}
repoSpec := fmt.Sprintf("%s/%s", repoInfo.Owner.Login, repoInfo.Name)
@@ -820,13 +821,21 @@ func createPR(branchName, title, body string, verbose bool) error {
if err != nil {
// Try to get stderr for better error reporting
if exitError, ok := err.(*exec.ExitError); ok {
- return fmt.Errorf("failed to create PR: %w\nOutput: %s\nError: %s", err, string(output), string(exitError.Stderr))
+ return 0, "", fmt.Errorf("failed to create PR: %w\nOutput: %s\nError: %s", err, string(output), string(exitError.Stderr))
}
- return fmt.Errorf("failed to create PR: %w", err)
+ return 0, "", fmt.Errorf("failed to create PR: %w", err)
}
prURL := strings.TrimSpace(string(output))
- fmt.Printf("π’ Pull Request created: %s\n", prURL)
- return nil
+ // Parse PR number from URL (e.g., https://github.com/owner/repo/pull/123)
+ prNumber := 0
+ parts := strings.Split(prURL, "/")
+ if len(parts) > 0 {
+ if num, parseErr := strconv.Atoi(parts[len(parts)-1]); parseErr == nil {
+ prNumber = num
+ }
+ }
+
+ return prNumber, prURL, nil
}
diff --git a/pkg/cli/trial_repository.go b/pkg/cli/trial_repository.go
index 3c2c73eea8..2ceae1c32b 100644
--- a/pkg/cli/trial_repository.go
+++ b/pkg/cli/trial_repository.go
@@ -202,7 +202,7 @@ func installWorkflowInTrialMode(tempDir string, parsedSpec *WorkflowSpec, logica
}
// Add the workflow from the installed package
- if err := AddWorkflows([]string{parsedSpec.String()}, 1, verbose, "", "", true, appendText, false, false, false, "", false, ""); err != nil {
+ if _, err := AddWorkflows([]string{parsedSpec.String()}, 1, verbose, "", "", true, appendText, false, false, false, "", false, ""); err != nil {
return fmt.Errorf("failed to add workflow: %w", err)
}
}
diff --git a/pkg/cli/update_command_test.go b/pkg/cli/update_command_test.go
index 7dde1aba2f..17560f6252 100644
--- a/pkg/cli/update_command_test.go
+++ b/pkg/cli/update_command_test.go
@@ -676,7 +676,7 @@ This is a test workflow.
// Test with refreshStopTime=false (should preserve existing stop time if lock exists)
t.Run("compileWorkflowWithRefresh false", func(t *testing.T) {
- err := compileWorkflowWithRefresh(workflowFile, false, "", false)
+ err := compileWorkflowWithRefresh(workflowFile, false, false, "", false)
if err != nil {
t.Logf("Compilation failed (expected in test environment): %v", err)
// In a test environment without full setup, compilation may fail,
@@ -686,7 +686,7 @@ This is a test workflow.
// Test with refreshStopTime=true (should regenerate stop time)
t.Run("compileWorkflowWithRefresh true", func(t *testing.T) {
- err := compileWorkflowWithRefresh(workflowFile, false, "", true)
+ err := compileWorkflowWithRefresh(workflowFile, false, false, "", true)
if err != nil {
t.Logf("Compilation failed (expected in test environment): %v", err)
// In a test environment without full setup, compilation may fail,
diff --git a/pkg/cli/update_workflows.go b/pkg/cli/update_workflows.go
index 6c7ab6508f..3556d68c7b 100644
--- a/pkg/cli/update_workflows.go
+++ b/pkg/cli/update_workflows.go
@@ -454,7 +454,7 @@ func updateWorkflow(wf *workflowWithSource, allowMajor, force, verbose bool, eng
// Compile the updated workflow with refreshStopTime enabled
updateLog.Printf("Compiling updated workflow: %s", wf.Name)
- if err := compileWorkflowWithRefresh(wf.Path, verbose, engineOverride, true); err != nil {
+ if err := compileWorkflowWithRefresh(wf.Path, verbose, false, engineOverride, true); err != nil {
updateLog.Printf("Compilation failed for workflow %s: %v", wf.Name, err)
return fmt.Errorf("failed to compile updated workflow: %w", err)
}
diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go
index f47dffe1a5..049c20379a 100644
--- a/pkg/constants/constants.go
+++ b/pkg/constants/constants.go
@@ -560,6 +560,33 @@ const (
// Note: This remains a string slice for backward compatibility with existing code
var AgenticEngines = []string{string(ClaudeEngine), string(CodexEngine), string(CopilotEngine)}
+// EngineOption represents a selectable AI engine with its display metadata and secret configuration
+type EngineOption struct {
+ Value string
+ Label string
+ Description string
+ SecretName string // The name of the secret required for this engine (e.g., "COPILOT_GITHUB_TOKEN")
+ EnvVarName string // Alternative environment variable name if different from SecretName (optional)
+ KeyURL string // URL where users can obtain their API key (empty for engines with special setup like Copilot)
+}
+
+// EngineOptions provides the list of available AI engines for user selection
+var EngineOptions = []EngineOption{
+ {string(CopilotEngine), "GitHub Copilot", "GitHub Copilot CLI with agent support", "COPILOT_GITHUB_TOKEN", "", ""},
+ {string(ClaudeEngine), "Claude", "Anthropic Claude Code coding agent", "ANTHROPIC_API_KEY", "", "https://console.anthropic.com/settings/keys"},
+ {string(CodexEngine), "Codex", "OpenAI Codex/GPT engine", "OPENAI_API_KEY", "", "https://platform.openai.com/api-keys"},
+}
+
+// GetEngineOption returns the EngineOption for the given engine value, or nil if not found
+func GetEngineOption(engineValue string) *EngineOption {
+ for i := range EngineOptions {
+ if EngineOptions[i].Value == engineValue {
+ return &EngineOptions[i]
+ }
+ }
+ return nil
+}
+
// DefaultReadOnlyGitHubTools defines the default read-only GitHub MCP tools.
// This list is shared by both local (Docker) and remote (hosted) modes.
// Currently, both modes use identical tool lists, but this may diverge in the future
diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go
index e448f2ebd0..d4a3788a90 100644
--- a/pkg/workflow/compiler.go
+++ b/pkg/workflow/compiler.go
@@ -619,17 +619,19 @@ func (c *Compiler) CompileWorkflowData(workflowData *WorkflowData, markdownPath
}
}
- // Display success message with file size if we generated a lock file
- if c.noEmit {
- fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(console.ToRelativePath(markdownPath)))
- } else {
- // Get the size of the generated lock file for display
- if lockFileInfo, err := os.Stat(lockFile); err == nil {
- lockSize := console.FormatFileSize(lockFileInfo.Size())
- fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("%s (%s)", console.ToRelativePath(markdownPath), lockSize)))
- } else {
- // Fallback to original display if we can't get file info
+ // Display success message with file size if we generated a lock file (unless quiet mode)
+ if !c.quiet {
+ if c.noEmit {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(console.ToRelativePath(markdownPath)))
+ } else {
+ // Get the size of the generated lock file for display
+ if lockFileInfo, err := os.Stat(lockFile); err == nil {
+ lockSize := console.FormatFileSize(lockFileInfo.Size())
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("%s (%s)", console.ToRelativePath(markdownPath), lockSize)))
+ } else {
+ // Fallback to original display if we can't get file info
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(console.ToRelativePath(markdownPath)))
+ }
}
}
return nil
diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go
index 976b6bfb9d..1887dffa0e 100644
--- a/pkg/workflow/compiler_types.go
+++ b/pkg/workflow/compiler_types.go
@@ -17,6 +17,7 @@ type FileTracker interface {
// Compiler handles converting markdown workflows to GitHub Actions YAML
type Compiler struct {
verbose bool
+ quiet bool // If true, suppress success messages (for interactive mode)
engineOverride string
customOutput string // If set, output will be written to this path instead of default location
version string // Version of the extension
@@ -89,6 +90,11 @@ func (c *Compiler) SetSkipValidation(skip bool) {
c.skipValidation = skip
}
+// SetQuiet configures whether to suppress success messages (for interactive mode)
+func (c *Compiler) SetQuiet(quiet bool) {
+ c.quiet = quiet
+}
+
// SetNoEmit configures whether to validate without generating lock files
func (c *Compiler) SetNoEmit(noEmit bool) {
c.noEmit = noEmit
diff --git a/socials/PLAN.md b/socials/PLAN.md
index c1545e1810..a13c5eb6ee 100644
--- a/socials/PLAN.md
+++ b/socials/PLAN.md
@@ -77,7 +77,6 @@ Remaining posts (shifted later due to daily Meet the Workflows roll-out):
- 2026-02-14 -> `25-security-lessons.md`
- 2026-02-15 -> `26-how-workflows-work.md`
- 2026-02-16 -> `27-authoring-workflows.md`
-- 2026-02-17 -> `28-getting-started.md`
### Content Format
diff --git a/socials/campaign.log b/socials/campaign.log
index 04ca052192..9b3d6aafe9 100644
--- a/socials/campaign.log
+++ b/socials/campaign.log
@@ -71,7 +71,6 @@
[2026-01-16 21:32:40] Content scheduled for 2026-02-14 but not yet created: /home/dsyme/gh-aw/socials/content/25-security-lessons.md
[2026-01-16 21:32:40] Content scheduled for 2026-02-15 but not yet created: /home/dsyme/gh-aw/socials/content/26-how-workflows-work.md
[2026-01-16 21:32:40] Content scheduled for 2026-02-16 but not yet created: /home/dsyme/gh-aw/socials/content/27-authoring-workflows.md
-[2026-01-16 21:32:40] Content scheduled for 2026-02-17 but not yet created: /home/dsyme/gh-aw/socials/content/28-getting-started.md
[2026-01-16 21:32:40] Tracking engagement for recent posts
[2026-01-16 21:33:02] Running campaign
[2026-01-16 21:33:02] [DRY RUN] No API calls will be made
diff --git a/socials/scripts.sh b/socials/scripts.sh
index ef428c0dab..b6f8d88c03 100644
--- a/socials/scripts.sh
+++ b/socials/scripts.sh
@@ -101,7 +101,6 @@ declare -A SCHEDULE=(
["2026-02-14"]="25-security-lessons.md"
["2026-02-15"]="26-how-workflows-work.md"
["2026-02-16"]="27-authoring-workflows.md"
- ["2026-02-17"]="28-getting-started.md"
)
# Map content files to schedule